Elemar DEV

Negócios, tecnologia e desenvolvimento

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

Recapitulando

Essa série objetiva construir um framework simples para criação de mocks e proxies. O que já vimos até agora foi:

Sobre essa parte

Nessa parte faremos duas coisas:

  1. daremos suporte a callbacks para a chamada de métodos;
  2. permitiremos que nossos objetos mock implementem mais de uma interface.

Para exemplificar esses conceitos, observe dois dos métodos de teste que escrevi para essas características:

[TestMethod]
public void CallbackBeforeAfter()
{
    // arrange
    var before = false;
    var after = false;

    var target = new FluentMock<IFoo>()

        .Setup((foo) => foo.Process(It.IsAny<int>()))
        .Callback(() => before = true)
        .Returns(1)
        .Callback(() => after = true)

        .CreateObject();

    // act
    var result = target.Process(10);

    // Assert
    Assert.AreEqual(1, result);
    Assert.IsTrue(before, "before should be true");
    Assert.IsTrue(after, "after should be true");
}

[TestMethod]
public void MultipleInterfacesDifferentSetups()
{
    // arrange
    bool ifoo = false;
    bool ifoo2 = false;
    var target = new FluentMock()
            .As<IFoo>()
                .Setup( (foo) => foo.DoSomething() )
                .Callback( () => ifoo = true )
            .As<IFoo2>()
                .Setup((foo) => foo.DoSomething2())
                .Callback( () => ifoo2 = true )
            .CreateObject();

    // act
    ((IFoo)target).DoSomething();
    ((IFoo2)target).DoSomething2();

    // assert
    Assert.IsTrue(ifoo, "Interface IFoo was not invoked");
    Assert.IsTrue(ifoo2, "Interface IFoo2 was not invoked");
}

Legal, né? Smiley piscando

Se desejar, obtenha o código-fonte desse post aqui

Uma palavrinha sobre testes

Não sou adepto (ainda) de TDD. Mas, sou simpático a idéia de testes.

Quando comecei esse projeto não percebi necessidade imediata de testes. Entretanto, ouvindo o fantástico postcast sobre testes da DotNetArchitects, percebi que estava indo na direção errada. Minha impressão foi sendo reforçada pela dificuldade de mostrar exemplos de aplicação do mini-framework (especificação) que estamos desenvolvendo e pela insegurança que estava começando a sentir para promover as alterações. Em meus testes manuais (caros e ineficientes, conforme Giovanni Bassi), encontrei algumas surpresas desagradáveis: Meu código “parado”, sem mais ou menos, começou a quebrar sem que eu conseguisse achar explicação imediata.

Por isso, agora nosso mini-framework conta com um projeto de testes (EasyMock.Tests). Nesse projeto, tentei cobrir os pontos que considerava fundamentais. Caso você encontre algum “bug” nessa liberação, me informe que farei o fix e programarei o teste adequado. Caso deseje colaborar com novos testes, mande seu código para elemarjr@gmail.com.

Algum refactoring

Como usual, antes de implementar novas características promovi algum refactoring no código. Muitas das alterações não são tão relevantes para serem explicitadas em detalhes aqui. Mesmo assim, para você que está acompanhando o código em cada post, as relaciono a seguir:

  • A criação dos objetos Match, partindo dos argumentos dos métodos, foi deslocada da classe FluentMock para o construtor da classe MatchCollection (O que simplificou muito a classe base MethodCall)
  • Criei um novo namespace, chamado EasyMock.Fluent.Internals, para acomodar todas as classes que não devem ser instanciadas diretamente pelo programador cliente. Isso diminuiu a quantidade de classes mostrada pelo VS quando vou criar um objeto;
  • Visando eliminar dificuldades de utilização, revisei os métodos e propriedades das classes FluentMock, MethodCall e derivadas, mudando a visibilidade de Public para Internal sempre que possível.

Adicionando suporte fluente a mais de uma interface

Pelo que você pode ver no código de teste, o suporte a mais de uma interface é provido mediante o uso do método genérico As. Como esse método funciona? qual é a lógica dele? Onde ele está? Bem, respondendo as perguntas começando pela última: o método está localizado na classe FluentMock que, agora, deixou de ser abstract. Para responder as demais perguntas, mostremos o código que está nesse método:

	internal FluentMock Ancestor { get; set; }

        public FluentMock<T> As<T>()
            where T : class
        {
            var result = new FluentMock<T>(this.Handler);
            result.Ancestor = this;
            return result;
        }

Tipos especificados em interface fluente. Problema resolvido!! Smiley piscando Mas, ainda não implementamos nada. O “pulo” do gato desse método está em devolver uma “instância fluente” especialista para o tipo solicitado. Observe que a instância atual é preservada na propriedade Ancestor do novo FluentMock e o Handler é repassado.

Recuperando a lista de todas as interfaces (types) informadas via fluência

Bem, nossa interface fluente já suporta que o programador cliente configure todos os tipos usando o método genérico As. Além disso, sabemos que todas as configurações serão acumuladas em um único FluentMockHandler. Também sabemos que todas as instâncias de FluentMock (e derivados) que são criadas são armazenadas na lista encadeada mascarada pela propriedade Ancestor. Agora é hora de conseguirmos relacioanar todas as interfaces apropriadas na fluência. Como fazemos isso? Bem, em partes:

  1. FluentMock define um método virtual, GetSupportedInterface, que retorna null
  2. FluentMock<T> sobrescreve esse método retornando typeof(T)
  3. FluentMock<T> define um método de apoio, GetAllInterfaces, que fornece uma enumeração dos tipos suportados pela instância atual e por todos os Ancestors.

Veja o código fonte de GetAllInterfaces

	private IEnumerable<Type> GetAllInterfaces()
        {
            List<Type> result = new List<Type>();
            FluentMock work = this;
            while (work != null)
            {
                var t = work.GetSupportedInterface();
                if (t != null && !result.Contains(t)) result.Add(t);
                work = work.Ancestor;
            }
            return result;
        }

Obtenção dos tipos informados na fluência resolvido!! Smiley piscando Mas, ainda não resolvemos nada. Agora é necessário criar efetivamente o objeto obedecendo essas interfaces.

Criando um objeto que realiza diversas interfaces

Bem, chegamos ao limite do que poderíamos alterar na nossa interface fluente. Partindo de agora, precisamos trabalhar em nossa camada de nível mais baixo. Antes disso, repare que o método CreateObject de FluentMock<T> mudou. Observe o código novo:

	public T CreateObject()
        {
            return (T)Mock.CreateInstance(this.Handler, this.GetAllInterfaces());
        }

Basicamente, chamamos uma nova sobrecarga de CreateInstance, da classe Mock, que recebe uma enumeração de tipos como entrada.

O código-fonte para CreateInstance, da classe Mock, está em Mock.cs. Segue transcrição:

        public static TInterface CreateInstance(MockHandler handler) 
        {
            return (TInterface)CreateInstance(handler, typeof(TInterface));
        }

        public static object CreateInstance(MockHandler handler, Type type)
        {
            return CreateInstance(handler, new Type[] { type });
        }

        public static object CreateInstance
            (MockHandler handler, IEnumerable types)
        {
            var tbuilder = BuilderHelpers.CreateTypeBuilder(types.ToArray(),
                "NewAssembly" + Guid.NewGuid().ToString(),
                "NewType" + Guid.NewGuid().ToString());

            hfield = tbuilder
                .DefineField("HandlerField", typeof(MockHandler),
                FieldAttributes.Private);

            var builder = tbuilder
                .CreateDefaultConstructor();

            var atypes = types.ToArray();
            for (int i = 0; i < atypes.Length; i++)
			    builder.LoadMethodsFrom(atypes[i]);
                
            var resultType = builder.CreateType();

            return Activator.CreateInstance(resultType, 
                new object[] { handler }); 
        }

Problema resolvido! Smiley de boca aberta Importante destacar que as alterações ocorridas em FluentIL foram apenas simplificações da própria interface do .net

Criação de objeto que realiza as interfaces informadas na fluência resolvida!! Observe que percorro todos os tipos passados como parâmetro emitindo seus métodos no novo tipo.

Suporte a Callbacks

Outra novidade introduzida no código desse post foi o método Callback. Algumas regras gerais precisam ser compartilhadas:

  1. Método Callback deve estar acessível, na fluência, apenas após o método Setup;
  2. O método não deve ficar disponível, na fluência, após o método Throws;
  3. O método deverá ficar disponível, na fluência, após o método Returns.

Alterações dos métodos Setup para suporte à Callback

Os métodos Setup, definidos em FluentMock<T> , foram modificados para suportar Callback.

Observe:

	 public MethodCall<FluentMockEx<T>, TResult> Setup<TResult>
            (Expression<Func<T, TResult>> func)
        {
            var methodCall = func.ToMethodCall();
            this.CheckMethodCall(methodCall);

            FluentMockEx<T> adapter = new FluentMockEx<T>(this.Handler);
            adapter.Ancestor = this.Ancestor;
            var result = new MethodCall<FluentMockEx<T>, TResult>
                (adapter, methodCall.Method, methodCall.Arguments.ToArray()); 
            adapter.CurrentMethodCall = result;

            return result;
        }

        public MethodCall<FluentMockEx<T>> Setup(Expression<Action>> action)
        {
            var methodCall = action.ToMethodCall();
            this.CheckMethodCall(methodCall);

            FluentMockEx<T> adapter = new FluentMockEx<T>(this.Handler);
            
            var result = new MethodCall<FluentMockEx<T>>();
                (adapter, methodCall.Method, methodCall.Arguments.ToArray());
            adapter.CurrentMethodCall = result;
            adapter.Ancestor = this.Ancestor;

            return result;
        }

A grande novidade é a criação de um adaptador para a classe FluentMock<T>. Trata-se da classe FluentMockEx<T>.  Basicamente, essa nova classe implementa opções para Throws. Mas, para que ela existe? Bem, apenas para permitir “callback before” no Setup métodos void.

Método Callback depois do método Setup

Para adicionar o método Callback depois do método Setup, incluímos uma definicão em MethodCall<TFluentmock>, usada pela interface fluente para chamada de métodos void, e outra em MethodCall<TFluentmock, TResult> usada para métodos com retorno.

Essa é a implementação de MethodCall<TFluentmock>, acionada pelo método Setup que configura métodos com retorno void.

	 public TFluentMock Callback(Action callback)
        {
            this.FluentMock.Handler.IncludeInterceptor(this);
            this.AppendInProcessAction(callback);
            return this.FluentMock;
        }

Importante evidenciar que TFluentmock, instanciado para essa classe é uma especialização de FluentMock, chamada FluentMockEx, que adiciona comportamentos de MethodCall à FluentMock. Isso significa que o programador-usuário poderá, na fluência, requisitar outro Callback ou Throws.

E essa é a implementação de MethodCall<TFluentmock, TResult>, devidamente assinaliado como new:

	 public new MethodCall<TFluentmock, TResult> Callback(Action callback)
        
            this.FluentMock.Handler.IncludeInterceptor(this);
            this.AppendInProcessAction(callback);
            return this;
        }

Pelo retorno, o programador-usuário poderá, na fluência, requisitar outro Callback, Return ou Throws.

AppendInProcessAction: Adicionando callback a ação do método

Como sabemos, cada Setup adiciona um MethodCall a FluentMockHandler. Esse MethodCall possui uma propriedade, ProcessAction, que é do tipo um Func<string, object[]>, que contém a definição da função que deverá ser executada para aquela configuração.

O suporte a Callbacks é realizado pela definição dinâmica dessa Func. No lugar de definir o valor da propriedade diretamente, tanto os métodos Returns e Throws, quanto o método Callback, chamam um método de apoio AppendProcessInAction. Abaixo segue sobrecargas desse método:

	protected void AppendInProcessAction(Func<string, object[], object> func)
        {
            if (this.ProcessAction == null)
                this.ProcessAction = func;
            else
            {
                var old = this.ProcessAction;
                this.ProcessAction = (s, o) =>
                    {
                        old(s, o);
                        return func(s, o);
                    };
            }
        }

        protected void AppendInProcessAction(Action action)
        {
            if (this.ProcessAction == null)
            {
                this.ProcessAction = (s, o) =>
                    {
                        action();
                        return null;
                    };
            }
            else
            {
                var old = this.ProcessAction;
                this.ProcessAction = (s, o) =>
                    {
                        var result = old(s, o);
                        action();
                        return result;
                    };
            }
              
        }

Basicamente, os métodos apresentados redefinem a propriedade ProcessAction. Se esta propriedade ainda for null, o valor sugerido lhe é atribuído diretamete. Caso contrário, uma nova Func é definida, chamando o que já foi definido antes e a nova definição.

Conclusões

Esse post foi tremendamente denso. Honestamente, não estou satisfeito com as explicações que fiz. Mas, sejamos práticos, fizemos muitos avanços e chegamos a importantes conclusões:

  1. testes ajudam na manutenção de projetos;
  2. métodos fluentes podem adicionar complexidade ao desenvolvimento do framework;
  3. Expressions são recursos poderosos para customização (code as data);
  4. visibilidade dos membros não deve ser tratada de forma descuidada.

Sempre lembrando, o código-fonte está disponível aqui.

Bem, por hoje é só! Até mais Smiley piscando

Anúncios

6 comentários em “Faça você mesmo o seu framework para Mocks e Proxies–Parte 5

  1. Pingback: Faça você mesmo o seu framework para Mocks e Proxies–Parte 6 « Elemar DEV

  2. Pingback: Faça você mesmo o seu framework para Mocks e Proxies–Parte 7 « Elemar DEV

  3. Pingback: Faça você mesmo o seu framework para Mocks e Proxies–Parte 8 « Elemar DEV

  4. Pingback: Faça você mesmo o seu framework para Mocks e Proxies–Parte 9–Verificações « Elemar DEV

  5. Pingback: Faça você mesmo o seu framework para Mocks e Proxies–Parte 10–Suporte básico a eventos « Elemar DEV

  6. Pingback: Como se fosse a primeira vez… « Elemar DEV

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s

Informação

Publicado às 12/08/2010 por em Post, Proxy e marcado , , .

Estatísticas

  • 917.969 hits
%d blogueiros gostam disto: