Posts Tagged ‘jMock’

Implementando testes com mock objects

July 20th, 2009

Ok, estou convencido que usar mock objects é uma boa idéia mas como faço isso? A princípio não é qualquer código que está pronto para ser testado usando-se mocks, ele precisa ser escrito de uma forma a possibilitar isso.

No geral escrever código de tal forma que seja testável é uma boa prática e indica que ele segue alguns princípios de projeto que o tornam fracamente acoplado, aumentam sua manutenibilidades, etc.

Vejamos um exemplo de código que deva ser alterado para se tornar testável:

public class CustomerFacade {
 
	private CustomerDao customerDao;
 
	public CustomerFacade() {
		customerDao = new CustomerDaoImpl();
	}
 
	public void hireCustomer(Custumer custumer) { ... }
 
	public void fireCustomer(Custumer custumer) { ... }
 
}

Como já devo ter dito, não sou muito bom de exemplos.. Mas acho que esse ai está bem ilustrativo, embora para alguns DAO possa parecer meio arcaico. Deixei as reticências para a imaginação de vocês.

Existem 3 formas de se alterar esse código, as duas primeiras se usam de técnicas de bom projeto e melhoram a qualidade do software:

1. Dependency Injection (DI)

O mais simples e limpo. Foi a forma usada para injetar os mocks no exemplo do post passado:

public class CustomerFacade {
 
	private CustomerDao customerDao;
 
	public void setCustomerDAO(CustomerDao customerDao) {
		this.customerDao = customerDao;
	}
 
	public void hireCustomer(Custumer custumer) { ... }
 
	public void fireCustomer(Custumer custumer) { ... }
}

Basta agora, na hora de testar injetar não o componente real, mas seu mock.

public class CustomerFacadeTest {
    private Mockery context = new Mockery();
 
    public CustomerFacadeTest() {
    	context = new Mockery();
    }
 
    @Test
    public void testHire() throws Exception {
        // set up mock
    	final CustomerDao customerDao = context.mock(CustomerDao.class);
 
        // test state
    	CustomerFacade customerFacade = new CustomerFacade();
    	customerFacade.setCustomerDAO(customerDao);
 
        // expectations
        context.checking(new Expectations(){{
	        oneOf(customerDao).save(12);
        }});
 
        // execute
        customerFacade.hireCustomer(new Custumer(12));
 
        // verify
        context.assertIsSatisfied();
    }
 
}

2. Factory Method

Uma técnica um pouco mais embolada mas que ainda assim trás melhorias para o código consiste na extração de um Factory Method:

public class CustomerFacade {
 
	private CustomerDao customerDao;
 
	public CustomerFacade() {
		customerDao = createCustomerDao();
	}
 
	protected CustomerDao createCustomerDao() {
		return new CustomerDaoImpl();
	}
 
	public void hireCustomer(Custumer custumer) { ... }
 
	public void fireCustomer(Custumer custumer) { ... }
 
}

Notem que o createCustomerDao() é protected, isso porque na hora de usá-lo nos testes vamos estender essa classe e sobrescrever o createCustomerDao() para retornar um mock:

public class CustomerFacadeTest {
    private Mockery context = new Mockery();
 
    public CustomerFacadeTest() {
    	context = new Mockery();
    }
 
    @Test
    public void testHire() throws Exception {
        // set up mock
    	final CustomerDao customerDao = context.mock(CustomerDao.class);
 
        // test state
    	CustomerFacade customerFacade = new CustomerFacade() {
 
		@Override
		protected CustomerDao createCustomerDao() {
			// return the mock instead!!
			return customerDao;
		}
 
    	};
 
        // expectations
        context.checking(new Expectations(){{
	        oneOf(customerDao).save(12);
        }});
 
        // execute
        customerFacade.hireCustomer(new Custumer(12));
 
        // verify
        context.assertIsSatisfied();
    }
 
}

3. Aspect Oriented Programming

Essa é uma técnica workaround, e é para quando realmente não há jeito de usar-se as outras técnicas. Com ela não é preciso alterar código algum, apenas criar um point cut que intercepte a instanciação do CustomerDaoImpl. O test case é o seguinte:

public class CustomerFacadeTest {
	private Mockery context = new Mockery();
	private CustomerDaoImpl customerDao;
 
	public CustomerFacadeTest() {
		context = new Mockery();
		// so we can mock a concrete class
		context.setImposteriser(ClassImposteriser.INSTANCE);
 
		customerDao = context.mock(CustomerDaoImpl.class);
	}
 
	// returns the mock instance in test context
	public CustomerDaoImpl getCustomerDao() {
		return customerDao;
	}
 
	@Test
	public void testHire() throws Exception {
		// test state
		CustomerFacade customerFacade = new CustomerFacade();
 
		// expectations
		context.checking(new Expectations(){{
			oneOf(customerDao).save(12);
		}});
 
		// execute
		customerFacade.hireCustomer(new Custumer(12));
 
		// verify
		context.assertIsSatisfied();
	}
 
	public String print() {
		return("adasda");
	}
 
}

E criando-se o aspecto em AspectJ:

public aspect InstanciatingAspect{
 
	// intercept only from CustomerFacadeTest
	pointcut testing(Object test) :
		this(test) &&
		execution(public void CustomerFacadeTest.*());
 
	// intercept CustomerDaoImpl instantiations
	pointcut instanciateCustomerDao(Object test) :
		cflow(testing(test)) &&
		call(CustomerDaoImpl.new(..));
 
	Object around(Object test) : instanciateCustomerDao(test) {
		// get a hold of the mock instance made public by our test case
		CustomerDaoImpl customerDaoImpl = ((CustomerFacadeTest) test).getCustomerDao();
		// return the mock reference instead
		return customerDaoImpl;
	}
}

Como visto, a única dificuldade é que o aspecto deve retornar uma instância do mock que tenha sido criada no contexto do test case.Para tanto instanciamos o mock no test case e o tornamos acessível através do getCustomerDao(). Feito isso, ao interceptarmos no aspecto a criação do CustomerDaoImpl() nós a substituímos pela referência obtida do test case.

Série de trabalhos: Mock Objects

May 2nd, 2009

Estou fazendo dois trabalhos atualmente: sobre Mock Objects (como preparatório para Service Simulation, se tudo der certo) e sobre WSS. Por isso, e também para receber alguns inputs (que seriam muito bem vindos) vou postar algumas coisas da minha pesquisa aqui no blog. E nesse post explico o que são Mock Objects.

Mock Objects são objetos que simulam outros objetos. São principalmente usados em testes de unidade. Existem vários frameworks de Mock Objects mas pessoalmente prefiro o jMock. Abaixo mostro um exemplo de um teste de unidade usando-se TestNG 5.9 e jMock 2.5:

public class PublisherTest {
	private Mockery context = new Mockery();
 
	public PublisherTest() {
		context = new Mockery();
		// para podermos mockar uma classe
		context.setImposteriser(ClassImposteriser.INSTANCE);
	}
 
	@Test
	public void testOneSubscriberReceivesAMessage() {
		// configura mock
		Subscriber subscriber = context.mock(Subscriber.class);
		Expectations expect = new Expectations();
		String message = "message";
 
		// configura estado do objeto
		Publisher publisher = new Publisher();
		publisher.add(subscriber);
 
		// expectativas
		expect.never(subscriber).receive(message);
 
		context.checking(expect);
 
		// executa
		publisher.publish(message);
 
		// verifica
		context.assertIsSatisfied();
	}
 
}

Usamos mocks em cenários onde precisamos testar um objeto que depende de outros. Qual o problema de dependências? O objetivo dos testes de unidade é testar objetos em isolado, mas se usássemos um Subscriber real erros nele poderiam fazer falhar o teste do Publisher (ver figura abaixo). Por isso usamos mocks.

Teste usando-se objetos reais

Teste usando-se objetos reais

Teste usando-se mocks

Teste usando-se mocks

Com mocks podemos não só substituir algum objeto, mas também controlar todas as interações que são feitas com ele: verificar chamadas de métodos, valores de parâmetros, definir valores de retorno, etc. No código acima estamos por exemplo esperando uma chamada ao método receive do subscriber.

Dito o objetivo que motivou a criação dessa técnica, existem outros que vieram a ser supridos por mocks:

  • Abstrair camadas mais baixas do sistema, tanto por elas não estarem prontas quanto por querermos trabalhar offline.
  • Centralizar a configuração de estado no próprio mock ao invés de espalhá-las pelos testes de unidade.
  • Simular condições difíceis de reproduzir, como o lançamento de uma excessão mais esdrúxula ou um valor retorno raro.
  • Verificar mais rapidamente quando um erro ocorre (fail fast), indicando em que passo do comportamento esperado foi errado ao invés de apenas dizer que o estado final do objeto não condiz com o esperado.

Algo muito interessante vindo do uso de mocks é o Need-Driven Development (NDD), mas não pretendo falar dele aqui. Quando sair meu trabalho disponibilizo no blog, mas quem tiver curiosidade pode me escrever, é sempre bom trocar idéias :)