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();
  }
}
}

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

  1. ptcmariano diz:

    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.

Deixar um comentário

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Modificar )

Imagem do Twitter

You are commenting using your Twitter account. Log Out / Modificar )

Facebook photo

You are commenting using your Facebook account. Log Out / Modificar )

Connecting to %s

Seguir

Get every new post delivered to your Inbox.