Melhorando a qualidade dos testes com refatoração e fluent interfaces

Muita gente acha que não precisa dar muita atenção à qualidade na escrita dos testes. Mas o nível de qualidade dos testes deve ser tão alto quanto o do código de produção, afinal, os testes também tem que ser mantidos por tanto tempo quanto o código de produção. No seu livro Clean Code, Uncle Bob conta a história de uma equipe que decidiu abrandar as regras de qualidade nos testes. Traduzindo livremente:

As variáveis não tinham que ser bem nomeadas, os métodos não tinham que ser pequenos e descritivos. O código de teste não tinha que ter um bom design. Contanto que o código de teste funcionasse e cobrisse o código de produção, estava tudo bem.

Quanto mais o tempo passava, mais difícil era alterar os testes e adicionar novos. Vez ou outra alguns testes falhavam quando a equipe mudava o código de produção, e corrigi-los também se tornou uma tarefa árdua e demorada. As estimativas ficaram cada vez mais altas, porque mexer nos testes era muito custoso. Depois de um tempo, eles jogaram fora toda a suite de testes. Mas sem os testes, muitos bugs começaram a aparecer, a equipe perdeu a confiança em alterar o código, e no final, clientes e desenvolvedores ficaram frustados.

Moral da história, se o código será mantido – seja de produção ou de testes – então ele tem que ser bem escrito.

Então, vamos botar em prática essa regra. A seguir, temos um exemplo de como podemos melhorar a qualidade de um teste. No último projeto que participei, uma classe de testes estava nos incomodando. Cada vez que tínhamos que alterá-la, era uma demora. Toda vez que abríamos essa classe, demorávamos entendendo novamente a sua difícil lógica. Vou mostrar a evolução do seguinte método de teste:

@Test
public void anActionOriginatedInClientShouldNotBeExecutedEvenIfReceivedFromServer() {

O código original:


actionSyncServiceTestUtils.getActionExecutionServiceMock().addActionExecutionListener(new ActionExecutionListener( {
    public void onActionExecution(final Action action, final ProjectContext context,
        final Set<UUID> inferenceInfluencedSet, final boolean isUserAction) {}
});

loadProjectContext(new ProjectContextLoadCallback() {
    public void onProjectContextLoaded(final ProjectContext context) {
        actionSyncServiceTestUtils.getActionExecutionServiceMock().onUserActionExecutionRequest(
            new Action(context.getProject().getId(), "filho"));
    }
    public void onProjectContextFailed(final Throwable caught) {
         assertEquals(SAME_CLIENT_EXCEPTION_MESSAGE, caught.getMessage());
    }
});

Depois da primeira refatoração:

final ArgumentCaptor<ActionSyncEvent> eventHandlerCaptor = getEventHandlerCaptor();

createInstance();

final ActionSyncEvent eventHandler = eventHandlerCaptor.getValue();

final UUID clientId = new UUID("123");
clientHasId(clientId);
try {
     eventHandler.onEvent(new ActionSyncEvent(createRequest(clientId)));
} catch (final RuntimeException e) {
     final String sameClientExceptionMessage = "This client received the same action it sent to server.";
     assertEquals(sameClientExceptionMessage, e.getMessage());
}

assertNoActionWasExecutedInClient();

Aqui já ficou bem melhor, mas o código de stubbing e os diferentes níveis de abstração e o bloco try-catch ainda o deixavam confuso. Nessa hora veio à mente esse post do Naresh Jain e percebemos que poderíamos fazer algo parecido com os nossos testes, inserindo fluent interfaces. E o resultado foi esse:

final String clientId = "123";
given().aClientWithId(clientId);
when().aRequestArriveFrom(clientId);
verifyThat().noActionWasExecutedInClient();

Aqui está outro exemplo:

@Test
public void anActionOriginatedInClientShouldBeExecutedOnce() throws Exception {
    final int nTimes = 1;
    when().anActionWasExecutedInClient();
    verifyThat().userActionsWereExecutedInClient(nTimes);
}

Assim fica bem fácil manter os testes, não é?

Segue o código completo abaixo.


public class ActionSyncServiceTest {
private ActionSyncService ASS;
private ActionSyncRequest request;

@Test
public void anActionOriginatedInClientShouldNotBeExecutedEvenIfReceivedFromServer() throws Exception {
    final String clientId = "123";
    given().aClientWithId(clientId);
    when().aRequestArriveFrom(clientId);
    verifyThat().noActionWasExecutedInClient();
}

@Test
public void anActionOriginatedInClientShouldBeExecutedOnce() throws Exception {
    final int nTimes = 1;
    when().anActionWasExecutedInClient();
    verifyThat().userActionsWereExecutedInClient(nTimes);
}

@Test
public void actionsNotOriginatedInClientShouldBeExecutedAfterBeingReceivedFromServer() throws Exception {
    final short projectId = 1;
    given().aClientWithId("client being tested").ignoringProjectCheck(projectId);
    when().aRequestArriveFrom("other client");
    verifyThat().nonUserActionsWereExecutedInClient();
}

@Test
public void anActionNotOriginatedInClientShouldNotBeExecutedAsClientActionAfterBeingReceivedFromServer() throws Exception {
    final short projectId = 1;
    given().aClientWithId("client being tested").ignoringProjectCheck(projectId);
    when().aRequestArriveFrom("other client");
    verifyThat().nonUserActionsWereExecutedInClient();
}

@Test
public void anActionReceivedFromServerThatBelongsToAProjectDifferentFromClientCurrentProjectShouldNotBeExecuted() throws Exception {
    final int currentProject = 1;
    final int otherProject = 2;
    given().aClientWithId("client").aClientWithCurrentProject(currentProject);
    when().aRequestArrivedFrom("other client", otherProject);
    verifyThat().noActionWasExecutedInClient();
}

@Test
public void anActionReceivedFromServerThatBelongsToTheSameProjectOfClientCurrentProjectShouldBeExecuted() throws Exception {
    final short project = 1;
    given().aClientWithId("client").aClientWithCurrentProject(project);
    when().aRequestArrivedFrom("other client", project);
    verifyThat().nonUserActionsWereExecutedInClient();
}

private Given given() {
    return new Given();
}

private When when() {
    return new When();
}

private VerifyThat verifyThat() {
    return new VerifyThat();
}

private class Given {
  private Given aClientWithId(final String clientId) {
    Mockito.when(clientIdentificationProvider.getClientId()).thenReturn(new UUID(clientId));
    return this;
  }

  private Given aClientWithCurrentProject(final int projectId) {
    Mockito.when(projectRepresentationProvider.getCurrentProjectRepresentation()).thenReturn(ProjectTestUtils.createRepresentation(projectId));
    return this;
  }

  private Given ignoringProjectCheck(final int projectId) {
    Mockito.when(projectRepresentationProvider.getCurrentProjectRepresentation()).thenReturn(ProjectTestUtils.createRepresentation(projectId));
    return this;
  }
}

private class When {
  private void aRequestArriveFrom(final String clientId) {
    request = RequestTestUtils.createActionSyncRequest(clientId);
    fireEvent();
  }

  private void aRequestArrivedFrom(final String clientId, final int projectId) {
    request = RequestTestUtils.createActionSyncRequest(clientId, projectId);
    fireEvent();
  }

  private void anActionWasExecutedInClient() {
    createInstance();
    final Action action = mock(Action.class);
    actionExecution.onUserActionExecutionRequest(action);
  }

  private void fireEvent() {
    final ArgumentCaptor<ServerActionSyncEventHandler> eventHandlerCaptor = getEventHandlerCaptor();
    createInstance();

    try {
      fireActionSynEvent(request,  eventHandlerCaptor);
    }
    catch (final RuntimeException e) {}
    }

  private void createInstance() {
     // Create instance of the class being tested.
  }

  private ArgumentCaptor<ServerActionSyncEventHandler> getEventHandlerCaptor() {
    final ArgumentCaptor<ServerActionSyncEventHandler> eventHandlerCaptor = ArgumentCaptor.forClass(ServerActionSyncEventHandler.class);
    doNothing().when(serverPush).registerServerEventHandler(eq(ServerActionSyncEvent.class), eventHandlerCaptor.capture());
    return eventHandlerCaptor;
  }

  private void fireActionSynEvent(final ActionSyncRequest request, final ArgumentCaptor<ServerActionSyncEventHandler> eventHandlerCaptor) {
    final ServerActionSyncEventHandler eventHandler = eventHandlerCaptor.getValue();
    eventHandler.onEvent(new ServerActionSyncEvent(request));
  }
}

private class VerifyThat {
  private void nonUserActionsWereExecutedInClient() throws Exception {
    final int nTimes = request.getActionList().size();
    verify(actionExecution, never()).onUserActionExecutionRequest(any(Action.class));
    verify(actionExecution, times(nTimes)).onNonUserActionRequest(any(Action.class));
    verify(actionExecution, never()).onUserActionRedoRequest();
    verify(actionExecution, never()).onUserActionUndoRequest();
  }

  private void noActionWasExecutedInClient() throws Exception {
    verify(actionExecution, never()).onUserActionExecutionRequest(any(Action.class));
    verify(actionExecution, never()).onNonUserActionRequest(any(Action.class));
    verify(actionExecution, never()).onUserActionRedoRequest();
    verify(actionExecution, never()).onUserActionUndoRequest();
  }

  private void userActionsWereExecutedInClient(final int nTimes) throws Exception {
    verify(actionExecution, never()).onNonUserActionRequest(any(Action.class));
    verify(actionExecution, times(nTimes)).onUserActionExecutionRequest(any(Action.class));
    verify(actionExecution, never()).onUserActionRedoRequest();
    verify(actionExecution, never()).onUserActionUndoRequest();
  }
}
}
About these ads

2 thoughts on “Melhorando a qualidade dos testes com refatoração e fluent interfaces

  1. Realmente muito importante deixar o código de fácil manutenção.
    Essa trabalho que foi feito é excelente.
    Valeu pelo post.

  2. Código de teste deve ser bem legível, pois é uma boa documentação. Além do mais, código de produção evolui em função dos testes, ou pelo menos deveria ser assim.
    Excelente post! Que venham mais.

O que tu achas?

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 )

Imagem do Twitter

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

Foto do Facebook

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

Foto do Google+

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

Conectando a %s