Elemar JR
23 agosto, 2010 0 Comentários AUTOR: elemarjr CATEGORIAS: Sem categoria Tags:, , ,

Faça você mesmo o seu framework para Mocks e Proxies–Parte 8

Olá pessoal. Tudo certin? Chegamos ao oitavo post da série sobre o desenvolvimento de um framework para proxies. Estou realmente orgulhoso do resultado. Embora EasyMock ainda não esteja em estágio de produção, já está sendo suficiente para os restes que escrevo.

Esta série começou como uma demonstração prática dos princípios que mostro na outra série desse blog: IL 101. Aos poucos o código foi “ganhando corpo”, minha referência ainda é o Moq, mas, honestamente, acho que a fluência que estou desenvolvendo já superou minha referência. Smiley piscando

Go Code!

Recapitulando

Se você está chegando agora, talvez deseje antes dar uma boa olhada no que já foi dito por aqui sobre o EasyMock. Os posts anteriores são:

Sobre essa parte

Bem, o objetivo inicial era adicionar fluência ao tratamento de propriedades. Inspirado no Moq, queria fazer isso pela adição de quatro métodos básicos:   SetupGet, SetupSet, SetupProperty e SetupAllProperties. Segue alguns códigos de exemplo demonstrando esses métodos:

1 var target = FluentMock.Create<IFooGetSetProperty>() 2 3 .SetupSet((foo) => foo.MyProperty2 = It.IsAny<string>()) 4 .Callback(() => callbackWasInvoked1 = true) 5 .Callback(() => callbackWasInvoked2 = true) 6 7 .SetupSet((foo) => foo.MyIntProperty2 = It.IsAny<int>()) 8 .Callback(() => callbackWasInvoked3 = true) 9 10 .SetupSet((foo) => foo.MyProperty2 = It.IsAny<string>()) 11 .Throws() 12 13 .SetupGet((foo) => foo.MyProperty2) 14 .Returns("Ok!") 15 16 .CreateObject();

ou ainda...

1 var target = new FluentMock<IFooGetSetProperty>() 2 .SetupAllProperties() 3 .CreateObject();

e ainda ...

1 var target = new FluentMock<IFooGetSetProperty>() 2 .SetupProperty((foo) => foo.MyIntProperty2, 10) 3 .SetupProperty((foo) => foo.MyProperty2) 4 .CreateObject();

Se você sabe como o Moq funciona, então está habituado com esses métodos. Se não sabe, cabe uma explicação breve:

  • SetupSet e SetupGet – permitem que sejam definidos os comportamentos do mock quando o get, ou set, de um método for acionado. Os comportamentos disponíveis são: lançamento de exception (Throws), retorno programado (Returns, só para o Get) ou ainda execução de um código associado (Callback);
  • SetupProperty – Ativa o comportamento padrão de propriedade, para a propriedade especificada. Ou seja, a propriedade armazena e recupera valores normalmente. Observe que um valor inicial pode ser especificado;
  • SetupAllProperties – Ativa comportamento de propriedade para todas as propriedades do tipo.

Bem, vamos lá. Já falei o que fiz. Agora vou demonstrar como Smiley de boca aberta. Mas antes, uma advertência: É bem difícil mostrar tudo que fiz, por isso, pegue o código-fonte da verão atual do EasyMock

Melhorando a experiência da interface fluente

Você leu meu post "Simplificando o IntelliSense para interfaces fluentes”? Então, apliquei o conceito aqui. Como foi feito? Primeiro escrevi a interface:

1 using System; 2 using System.ComponentModel; 3 using System.Diagnostics.CodeAnalysis; 4 5 namespace EasyMock.Fluent.Interfaces 6 { 7 [EditorBrowsable(EditorBrowsableState.Never)] 8 public interface IHideObjectMembers 9 { 10 [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] 11 [SuppressMessage("Microsoft.Naming", 12 "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "GetType")] 13 [EditorBrowsable(EditorBrowsableState.Never)] 14 Type GetType(); 15 16 [EditorBrowsable(EditorBrowsableState.Never)] 17 int GetHashCode(); 18 19 [EditorBrowsable(EditorBrowsableState.Never)] 20 string ToString(); 21 22 [EditorBrowsable(EditorBrowsableState.Never)] 23 bool Equals(object other); 24 } 25 }

Depois, apliquei nos objetos relacionados com a fluência:

1 using System; 2 using System.Linq.Expressions; 3 using EasyMock.Fluent.Internals; 4 5 namespace EasyMock.Fluent.Interfaces 6 { 7 public interface IFluentMock : IHideObjectMembers 8 { 9 IFluentMock<T> As<T>() 10 where T : class; 11 } 12 13 public interface IFluentMock<T> : IFluentMock 14 where T : class 15 { 16 IFluentMock<T> SetupProperty<TProperty> 17 (Expression<Func<T, TProperty>> property); 18 19 IFluentMock<T> SetupProperty<TProperty> 20 (Expression<Func<T, TProperty>> property, TProperty initialValue); 21 22 IFluentMock<T> SetupAllProperties(); 23 24 MethodCall<FluentMockEx<T>, TResult> Setup<TResult> 25 (Expression<Func<T, TResult>> func); 26 27 MethodCall<FluentMockEx<T>> Setup(Expression<Action<T>> action); 28 29 MethodCallWithFlexibleCallback<FluentMockEx<T>, TResult> 30 SetupGet<TResult>(Expression<Func<T, TResult>> action); 31 32 MethodCall<FluentMockEx<T>> SetupSet<TResult> 33 (Func<T, TResult> action); 34 35 T CreateObject(); 36 } 37 38 public interface IFluentMockEx<T> : IFluentMock<T> 39 where T : class 40 { 41 IFluentMockEx<T> Callback(Action callback); 42 43 44 IFluentMock<T> Throws<TException>() 45 where TException : Exception, new(); 46 47 IFluentMock<T> Throws(Exception e); 48 } 49 } 50

 

pare, extraí a interface das classes FluentMock. Ajuda a entender a coisa toda! Também apliquei a interface em FluentILGenerator e nos MethodCall (e associados).

Antes de continuar, uma modificação no Core

Na última parte, dei suporte a configuração de propriedades no Handler. Para isso, tinha adicionado três métodos em MockHandler (HandleGet, HandleSet e um outro que já esqueci o nome Smiley de boca aberta). Além disso, modifiquei o Emit para geração diferenciada de código para propriedades. Mas … não gostei!

Como mostrei em IL-101 – Parte 7. Por baixo dos panos, propriedades são “máscaras” para métodos (um para Get e outro para Set). Por isso, resolvi devolver MockHandler para seu estado original:

1 namespace EasyMock 2 { 3 public abstract class MockHandler 4 { 5 public abstract object HandleFunc(string methodname, object[] arguments); 6 public abstract void HandleAction(string methodname, object[] arguments); 7 } 8 }

Agora sim! Se você viu o post anterior, também removi todo o código de teste para os métodos que tinha escrito. Removi também o código que fazia o Emitting específico para propriedades e generalizei o método de Emitting para métodos para que escrevesse os get_* e set_* (geralmente, o padrão para métodos de apoio a propriedades).

Ou seja, praticamente descartei todo o trabalho que fiz para o post anterior. Deixando o choro de lado, destrui para construir Smiley de boca aberta

Extraindo código para execução dos comportamentos (Throws, Returns, Callback)

Leu Compondo a execução do programa usando Expressions? Se não, recomendo que dê uma olhada! Examinando o código fonte do EasyMock, perceberá que os métodos Returns, Throws e Callback usam essa técnica para configurar a execução do objeto Mock. Antes, o código estava misturado com o código da classe básica MethodCall. Agora está em casa nova:

1 using System; 2 3 namespace EasyMock.Fluent.Internals 4 { 5 internal class MethodCallActionRunner 6 { 7 public bool OffersResult { get; protected set; } 8 9 public MethodCallActionRunner(string methodName) 10 { 11 this.MethodNameField = methodName; 12 } 13 14 readonly string MethodNameField; 15 protected Func 16 ProcessActionField; 17 18 public void Add 19 (Func func) 20 { 21 if (this.ProcessActionField == null) 22 this.ProcessActionField = func; 23 else 24 { 25 var old = this.ProcessActionField; 26 this.ProcessActionField = (s, o) => 27 { 28 old(s, o); 29 return func(s, o); 30 }; 31 } 32 33 this.OffersResult = true; 34 } 35 36 public void Add(Action action) 37 { 38 if (this.ProcessActionField == null) 39 { 40 this.ProcessActionField = (s, o) => 41 { 42 action(); 43 return null; 44 }; 45 } 46 else 47 { 48 var old = this.ProcessActionField; 49 this.ProcessActionField = (s, o) => 50 { 51 var result = old(s, o); 52 action(); 53 return result; 54 }; 55 } 56 } 57 58 internal object Run(object[] arguments) 59 { 60 return this.ProcessActionField(this.MethodNameField, arguments); 61 } 62 } 63 }

Bem legal Alegre! Observe a propriedade OffersResult. Ela fica verdadeira quando nosso Runner recebe uma Func. Nossa MethodCall ficou magrinha Smiley de boca aberta

1 public class MethodCall : IHideObjectMembers 2 { 3 internal MethodCall(MethodInfo method) 4 { 5 this.Method = method; 6 this.Runner = new MethodCallActionRunner(method.Name); 7 this.MatchCriteria = new MatchCollection(); 8 } 9 10 protected MethodCall(MethodInfo method, Expression[] arguments) : 11 this(method) 12 { 13 this.MatchCriteria.LoadArguments(arguments); 14 } 15 16 internal MethodInfo Method { get; private set; } 17 internal readonly MatchCollection MatchCriteria; 18 19 internal MethodCallActionRunner Runner; 20 21 internal bool Matches(string methodName, object[] arguments) 22 { 23 if (methodName != this.Method.Name) return false; 24 return (this.MatchCriteria.Matches(arguments)); 25 } 26 }

Finalmente, SetupProperty

O método SetupProperty será nosso ponto de partida. Ele está definido na classe FluentMock. Segue o código:

1 public IFluentMock<T> SetupProperty<TProperty> 2 (Expression<Func<T, TProperty>> property) 3 { 4 5 return this.SetupProperty<TProperty>(property, default(TProperty)); 6 } 7 8 public IFluentMock<T> SetupProperty<TProperty> 9 (Expression<Func<T, TProperty>> property, TProperty initialValue) 10 { 11 this.Handler.EnablePropertyBehavior 12 (this.GetPropertyInfo(this.GetPropertyName(property)), initialValue); 13 return this; 14 }

Repare que as duas sobrecargas são simples. Basicamente, elas chamam o método EnablePropertyBehavior. Onde a mágica de fato acontece:

1 internal void EnablePropertyBehavior 2 (PropertyInfo property, object initialValue) 3 { 4 PropertyBox pbox = new PropertyBox() { Value = initialValue }; 5 6 if (property.CanRead) 7 { 8 MethodCall getmethodcall = 9 new MethodCall(property.GetGetMethod()); 10 getmethodcall.Runner.Add((s, o) => pbox.Value); 11 this.Interceptors.Add(getmethodcall); 12 } 13 14 if (property.CanWrite) 15 { 16 MethodCall setmethodcall = 17 new MethodCall(property.GetSetMethod()); 18 setmethodcall.Runner.Add((s, o) => pbox.Value = o[0]); 19 var m = new Match<object>((o) => true); 20 setmethodcall.MatchCriteria.Add(m); 21 this.Interceptors.Add(setmethodcall); 22 } 23 }

O código é bem simples. Basicamente, adiciono tratamento para o método get e para o método set, associados a propriedade. O tipo PropertyBox é uma classe simples, apenas com a propriedade Value, que uso nos “runners”.

SetupAllProperties

O código associado a SetupAllProperties é o mais direto:

1 public IFluentMock<T> SetupAllProperties() 2 { 3 foreach (var property in typeof(T).GetProperties()) 4 this.Handler.EnablePropertyBehavior( 5 this.GetPropertyInfo(property.Name), 6 null); 7 return this; 8 } 9

Facin! Percorro todas as propriedades do tipo, e chamo o método EnablePropertyBehavior (o mesmo da seção anterior).

SetupGet

Agora sim. As coisas ganham um pouco mais de corpo. Repare o código:

1 public MethodCallWithFlexibleCallback<FluentMockEx<T>, TResult> 2 SetupGet<TResult> 3 (Expression<Func<T, TResult>> action) 4 { 5 var pinfo = this.GetPropertyInfo(this.GetPropertyName(action)); 6 this.Handler.EnablePropertyBehavior(pinfo, default(TResult)); 7 8 FluentMockEx<T> adapter = new FluentMockEx<T>(this.Handler); 9 10 var result = new MethodCallWithFlexibleCallback<FluentMockEx<T>, TResult> 11 (adapter, pinfo.GetGetMethod(), null); 12 13 adapter.CurrentMethodCall = result; 14 adapter.Ancestor = this.Ancestor; 15 16 return result; 17 }

A lógica aqui é bem parecida com o método Setup que foi desenvolvido para métodos sem retorno (void). A única novidade verdadeira é o retorno MethodCallWithFlexibleCallback. Basicamente, essa versão de MethodCall permite que a configuração do get encerre já na definição de um Callback. Veja o código fonte para mais informações.

Um log das operações executadas no Mock

Outra novidade interessante, incluída nessa versão é que o Handler mantém um log de todas as operações (chamadas para HandleAction e HandleFunc). Observve:

1 public HandleLogEntryCollection Log { get; private set; } 2 3 public override object HandleFunc(string methodname, object[] arguments) 4 { 5 this.Log.Add(new HandleLogEntry() 6 { 7 MethodName = methodname, 8 Arguments = arguments, 9 InvokedAsAction = false 10 }); 11 12 object result = null; 13 foreach (var method in this.GetMethodCall(methodname, arguments)) 14 { 15 var lastresult = method.Runner.Run(arguments); 16 if (method.Runner.OffersResult) result = lastresult; 17 } 18 19 return result; 20 } 21 22 public override void HandleAction(string methodname, object[] arguments) 23 { 24 this.Log.Add(new HandleLogEntry() 25 { 26 MethodName = methodname, 27 Arguments = arguments, 28 InvokedAsAction = true 29 }); 30 Console.WriteLine(methodname); 31 foreach (var method in this.GetMethodCall(methodname, arguments)) 32 method.Runner.Run(arguments); 33 34 }

Isso será especialmente útil nos próximos posts, quando eu irei adicionar suporte a verificação. ´

SetupSet

Bem… aqui está o maior desafio técnico desse post. Expressions não oferecem suporte a atribuição… e isso, complica bastante as coisas. Simplesmente não posso obter os dados da instrução de forma simples… Mas .. (e sempre tem um mas), nosso log mostra de cara sua utilidade. Observe o código:

1 public MethodCall<FluentMockEx<T>> SetupSet<TResult> 2 (Func<T, TResult> action) 3 { 4 FluentMockHandler justForLogHandler = new FluentMockHandler(); 5 T justForLogInstance = Mock.CreateInstance<T>(justForLogHandler); 6 var lastMatchBeforeLog = It.LastMatch; 7 8 action(justForLogInstance); 9 10 if (justForLogHandler.Log.Count == 0) 11 throw new ArgumentException(); 12 13 var last = justForLogHandler.Log.Last(); 14 15 var allSetMethods = from p in typeof(T).GetProperties() 16 where p.CanWrite 17 select p.GetSetMethod().Name; 18 19 if (!allSetMethods.Contains(last.MethodName)) 20 throw new ArgumentException(); 21 22 FluentMockEx<T> adapter = new FluentMockEx<T>(this.Handler); 23 24 var result = new MethodCall<FluentMockEx<T>> 25 (adapter, typeof(T).GetMethod(last.MethodName), null); 26 27 if (It.LastMatch != lastMatchBeforeLog) 28 result.MatchCriteria.Add(It.LastMatch); 29 else 30 result.MatchCriteria.Add(new MatchEquals(last.Arguments[0])); 31 32 adapter.CurrentMethodCall = result; 33 adapter.Ancestor = this.Ancestor; 34 35 return result; 36 }

O que foi feito? Bem, alguma manobra de código. Vejamos:

  • Em primeiro lugar, passei a aceitar um Action e não somente Expression. Assim, a operação de atribuição ficou válida;
  • Crio um handler temporário, que irei usar somente nesse escopo para coletar o log;
  • Salvo o último Match gerado na classe It, para depois poder verificar se foi usado na atribuição;
  • Executo a action que recebi por parâmetro;
  • Se não houverem operações registradas no Log, programador trapaceou, não é um set;
  • Se o método chamado não estiver relacionado com o Set de uma propriedade do tipo “mockeado”, programador trapaceou;
  • Crio um objeto MethodCall para o método Set relacionado
  • Gero o critério relacionado ao valor usado na atribuição.

Concluindo

Bem .. por hoje é isso. Sei que esse post ficou “denso”. Mas não achei forma simples de apresentar as mudanças. Se está interessado, pegue o código-fonte. Estou interessado em colaboração.

Até mais Alegre