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.


