Faça você mesmo o seu framework para Mocks e Proxies–Parte 10–Suporte básico a eventos

Publicado em 05/09/2010

2


Olá galera, tudo certin? Essa semana foi quase temática aqui no blog. Falei sobre eventos quase todos os dias Alegre. E hoje não vai ser diferente!

Como já disse na semana passada, estou realmente orgulhoso com os resultados dessa série. Tenho aprendido um bocado e tenho achado aqui um ótimo caminho para compartilhar o pouco que sei.

Na minha opinião, EasyMock ainda não está em estágio de produção, mas acho que você ficaria surpreso positivamente se começasse a usar esse pequeno “frameworkzinho” em seus testes.

Em alguns posts dessa série, faço referências diretas à Intermediate Language. Se você não se sente confortável com isso, recomendo que leia a outra série desse blog: IL 101.

Minha referência para desenvolvimento é o Moq, mas, honestamente, acho que a fluência que estou desenvolvendo já superou minha referência. Smiley piscando

Por fim, ficaria feliz de receber seu feedback. Lembre-se, todo o código fonte está disponível no Codeplex.

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

Mantendo a tradição, estou postando hoje mais um “incremento” ao EasyMock: suporte básico a eventos. Chamo de básico porque não estou adicionando suporte direto na fluência (o que pretendo fazer no próximo post). Por agora, tudo que eu espero é fazer com que objetos mock tenham suporte assinatura de eventos. Além disso, estou expandindo o mecanismo de verificação, desenvolvido no post anterior, para que o usuário do framework consiga “disparar o evento”. Observe código de exemplo:

class Program { static void Main(string[] args) { var target = new FluentMock<IFooEvent>() .CreateObject(); target.MyEvent += (s, a) => { Console.WriteLine("Entrei!"); }; MockAgent.Create(target) .RaiseEvent("MyEvent", new object[] {target, new EventArgs()}); Console.ReadLine(); } } public interface IFooEvent { event EventHandler MyEvent; }

Smiley piscando

Certo! Eu sei que esse suporte ainda está bem primitivo. Mas, é um começo.

Fundamentação teórica no funcionamento dos eventos

A coisa toda fica mais simples de entender quando sabemos como as coisas funcionam “por baixo do capô”. Se você não “saca” muito dos bastidores da CLR, para eventos, dê uma olhada no post que escrevi detalhando funcionamento de eventos na Intermediate Language.

Implementando o suporte a eventos

Como já destaquei, por “baixo do capô”, um evento nada mais é do que um atributo (variável de classe) do tipo MulticastDelegate, um método para adicionar delegates e outro para remoção. Simples assim!

Como nosso mecanismo varre todos os métodos da interface, emitindo código em Intermediate Language, acaba emitindo, sózinho, tratamento para os eventos. Então, bastanos adicionar "interceptors” adequados. Como fazemos isso? Primeiro, quando vou criar o objeto mock, varremos todas as interfaces carregadas na fluência (geralmente uma).

public T CreateObject() { this.LoadDefaultEventsInterceptors(); return (T)Mock.CreateInstance(this.Handler, this.GetAllInterfaces()); } private void LoadDefaultEventsInterceptors() { foreach (var t in this.GetAllInterfaces()) this.LoadDefaultEventsInterceptors(t); }

Legal! Smiley piscando Agora, o segredo de verdade está no método LoadDefaultEventsInterceptors(Type t). Vamos ao código:

private void LoadDefaultEventsInterceptors(Type t) { var events = t.GetEvents(); foreach (var e in events) { DelegateEnvelope envelope = new DelegateEnvelope(); this.LoadDefaultEventAddInterceptor(e, envelope); this.LoadDefaultEventRemoveInterceptor(e, envelope); this.Handler.SupportedEvents.Add(e.Name, envelope); } }

Nenhuma surpresa aqui. Basicamente:

  • obtenho a lista de eventos do tipo;
  • para cada evento, crio um envelope (é só uma classe, com uma única propriedade [Delegate] onde armazeno o delegate associado ao evento que estou “mockeando”);
  • para cada evento, crio um interceptor para o Add, e outro para o Remove. (Mostro os códigos para isso logo abaixo);
  • para cada evento, carrego e adiciono o interceptor adequado no Handler (instância de FluentMockHandler), para viabilizar o “Raise”.

Agora, o código que gera o interceptor para o Add (operação +=):

private void LoadDefaultEventAddInterceptor(EventInfo e, DelegateEnvelope envelope) { MethodCall addInterceptor = new MethodCall(e.GetAddMethod()); addInterceptor.MatchCriteria.Add(new MatchesToAnything()); addInterceptor.Runner.AddAction((s, args) => { if (envelope.Delegate == null) envelope.Delegate = (MulticastDelegate)((object[])args)[0]; else envelope.Delegate = (MulticastDelegate)Delegate.Combine( envelope.Delegate, (Delegate)((object[])args)[0] ); } ); this.Handler.IncludeInterceptor(addInterceptor); }

Repare que utilizo um pouquinho de clousure aqui. Obtenho o MethodInfo da operação de Add usando o método GetAddMethod() de EventInfo. Depois disso, crio um interceptor (MethodCall), que aceita qualquer parâmetro e, cujo ação, altera o valor da propriedade Delegate, seja mediante atribuição direta, seja através do método estático Combine.

O código que gera o interceptor para Remove (operação –=) é igualmente simples. Observe:

private void LoadDefaultEventRemoveInterceptor(EventInfo e, DelegateEnvelope envelope) { MethodCall addInterceptor = new MethodCall(e.GetRemoveMethod()); addInterceptor.MatchCriteria.Add(new MatchesToAnything()); addInterceptor.Runner.AddAction((s, args) => { envelope.Delegate = (MulticastDelegate)Delegate.Remove( envelope.Delegate, (Delegate)((object[])args)[0] ); } ); this.Handler.IncludeInterceptor(addInterceptor); }

A lógica aqui ficou bem parecida. Smiley piscando

Raising…

Já conseguimos “assinar” eventos. Além disso, sabemos que todos os eventos têm um registro de delegate no handler. O que fiz então?

  • alterei o nome da classe que criamos no post passado de MockVerifier para MockAgent (estou ampliando o escopo da classe);
  • adicionei um método RaiseEvent, que recebe o nome do evento que quero “disparar” e a lista de argumentos.

O código para o método Raise ficou assim:

public void RaiseEvent(string eventName, object[] args) { var evt = this.Handler.SupportedEvents[eventName]; evt.Delegate.DynamicInvoke(args); }

Depois de tudo, a coisa ficou simples. Não?

Hoje, não vou disponibilizar o código-fonte do EasyMock. Vou deixar para fazer isso depois de ter adicionado a fluência. Certo??

Bem, por hoje, é isso!

Etiquetado:, , ,
Publicado em: Post, Proxy