How to perform TDD on the Service layer

Asked

Viewed 1,652 times

13

I participate in a project that uses MVC layers with Hibernate framework persisting in a Postgres. Junit is used for testing and Mockito is used for mocking (I still have no knowledge and practice about it)

So I was wondering, how do you work the TDD with the service layer, for example how do you validate Hibernate Annotations.

What you do and what experience you can share?

  • If other people want to contribute to this post feel free even to disseminate knowledge!

2 answers

21

TDD or Unit Test?

There is a big difference between TDD and Unit Test. TDD is based on unit tests, but both concepts are not exactly synonymous.

TDD (Test Driven Development) is a development methodology that measures project progress according to test results.

Unitary Test is one of the types of testing that is usually done in software. There are also integration tests, system, user, load, performance tests and other.

A unit test should test a single scenario of a system method without relying on external resources, such as databases, settings, and other factors that may interfere with the result. In summary, the unit test should test only one thing.

TDD in practice

TDD works well for some types of projects, especially when requirements are well defined and it is possible to write each test before implementation. It’s the ideal case.

In practice, however, many of the requirements are evolving throughout the project, both due to changes in business, as well as the failure to elicitate and even due to the maturation of the users' understanding. This and other reasons prevent a "pure" TDD because it adds a overhead of very great effort and many teams cannot afford this luxury.

Another factor that hinders is the maturity of the team. It is no use teaching a junior to use Junit and thinking that he will be able to do TDD. It is not easy to create a testable system, which is one of the qualities of a good software architecture.

The important thing is to understand that TDD is not a silver bullet .

Unit testing in practice

Test the important features

What I’ve seen in the market and what has worked out is to focus the test on what is important. There is no time to test everything, so the focus should be on the central functionalities of the system and not on registrations (CRUD).

Many people have doubts about what to test, some suggest even testing framework routines or, in the case of Java, check if Hibernate is saving the data in the database. This drives the developer crazy and goes in the wrong direction, because in the end he will test things that should be working and leave aside the most important.

Unit test, but not so unitary

The test need not be exactly unit, in the sense of testing only one method.

I worked on a project that involves integration with over half a dozen systems. These are integrations for all tastes: Web Services, files, databases. Many of the systems are still in the process of change. It would be impossible to create mocks for everything and having to update the mocks each time a system changed.

So to quote an example of file import testing, I created a Junit method that performs the task (job) import and check if the record has been stored successfully. Although you end up testing a lot at once, this is being enough to ensure functionality.

It is not "sin" to access the database in unit test. The bad thing is having the test fail frequently if the tables are not in the state necessary for the correct execution. But nothing prevents initializing values in setup test. Or there is the option to use a framework such as Testng, where you can set the execution order. Actually, it’s not exactly the order, what you can define is that an X test method depends on the Y test you have run before. I particularly like the Testng.

The design counts a lot

Speaking specifically about your service layer, I can pass on the lessons that experience has brought me.

The most important thing is to always design your classes and methods so that they are testable. At first it is difficult and you should spend time with Refactoring. Make each important method or routine as sparse as possible with other routines and static settings. Abuse of Inversion of Control.

Follow an example I will give in the next topic

A fictional example

Let’s create a class responsible for importing a file. Suppose the first implementation is quite naive:

public class ImportadorArquivo {
    public void importar() {

        //carrega local da configuração
        File arquivo = new File(Configuracao.LOCAL_ARQUIVO);

        //lista com itens lidos do arquivo
        List<Entidade> entidadesLidas = new ArrayList<>();

        //vários comandos para ler e interpretar o arquivo, colocando itens na lista...
        String[] linhas = FileUtil.lerLinhas(arquivo);
        for (String linha : linhas) {
            Entidade e = new Entidade();
            //preenche entidade com os dados da linha...
            entidadesLidas.add(e);
        }

        //grava itens no banco
        for (Entidade e : entidadesLidas) {
            JPAUtil.getEntityManager().persist(e);
        }

    }
}

It’s a really bad method to test, right? Let’s refactor this class to make it more testable:

public class ImportadorArquivo {

    private File arquivo;
    private EntityManager em;

    //recebe arquivo em entity manager (IoC)
    public ImportadorArquivo(File arquivo, EntityManager em) {
        this.em = em;
        this.arquivo = arquivo;
    }

    public void importar() {
        List<Entidade> entidadesLidas = ler();
        salvar(entidadesLidas);
    }

    public List<Entidade> ler() {
        List<Entidade> entidadesLidas = new ArrayList<>();
        String[] linhas = FileUtil.lerLinhas(arquivo);
        for (String linha : linhas) {
            entidadesLidas.add(interpretar(linha));
        }
        return entidadesLidas;
    }

    public Entidade interpretar(String linhaArquivo) {
        Entidade e = new Entidade();
        //preenche entidade com os dados da linha...
        return e;
    }

    public void salvar(List<Entidade> entidadesLidas) {
        for (Entidade e : entidadesLidas) {
            em.persist(e);
        }
    }

}

Note that each method now has a well-defined and distinct action. This allows you to test each action individually.

Also note that the class receives the settings by parameter. This is a type of Control Inversion. This allows you to test the class without any framework or magic.

Imagine the potential of a good design if applied to the whole system?

Real examples

I’ve been working on some small libraries and frameworks using TDD, at least to some extent.

I’ll list here two recent and updated Github projects with approximately 90% unit testing coverage:

  • Myq: a library to organize, load, and process SQL queries in a Java project.
  • T-Rex: an Excel spreadsheet generator via templates and an Expression language.

While I know I still have a lot to learn and improve in my implementations, I suggest it’s a good exercise to look at the class design in these projects and how they allow unit tests to run without any framework mock.

Additional reading

For an interesting discussion on the subject, see my article The TDD is dead? and, if you understand English, watch the video Is TDD Dead?.

  • But for example in my case, validations are used by the annotatons in the model. How would you validate?

  • 1

    @Macario1983 It is not necessary to test the validation of the model, you just need to check if the annotations are correct, since Hibernate will validate for you. However, if you want to do so, you can force a manual validation with the ValidatorFactory. Take an example here.

  • It should be validated if such a field received such an annotation? If yes, which form would be good?

  • 1

    @Macario1983 For me it does not make much sense to validate it. But if you really want to do it, just use a little reflection. Example: Assert.assertNotNull( MinhaEntidade.getClass().getAnnotation(Anotacao.class));. This checks the annotation Anotacao for a class. Doing with methods is something similar. I researched a little about notes or ask another specific question about it, because it is a somewhat extensive subject.

  • I read your text and article and thank you, for the ideas, I will see your text code, I also played with Testng and liked it very much but as I am new in development I am looking for inspirations and references to develop.

0

You mentioned tests on the Service layer but mentioned, as an example, tests with Hibernate Annotations.

In advance I say that I cannot imagine the use of Hibernate Annotations in the Service layer, unless your unit test is transposing the code of the Service itself and reaching the code of the entity and consequently the entities and saving them in some bank. In this case, it would not be a unit test, but a integration test.

Having said that, I will use an example of a more classic Service. As we are talking about TDD, let’s not start with the Service but by the test of it. It will be the test that will guide our implementation.

But first, we need to define the requirement on which this service will meet.

Example: Registration of person

As an example, let’s say that you need to insert a person into the database, validating whether their name is correct (it’s different from null) and whether they already exist in the database. If it already exists, it must return error.

Let’s create the PessoaServiceTest with the following test methods:

  • naoInserirPessoaSeNomeInvalido
  • naoInserirPessoaSePessoaExiste
  • inserirPessoaComSucesso

That are our test cases. It would be possible to think of more, but for example, let’s stick to these above.

As you can see, the methods have very clear names, explaining what they are testing.

In our PessoaServiceTest, let’s create the method naoInserirPessoaSeNomeInvalido. We expect, in this case, some kind of Exception, such as NomePessoaInvalidoException:

@Test(expect = NomePessoaInvalidoException.class)
public void naoInserirPessoaSeNomeInvalido() {

    PessoaService pessoaService = new PessoaService();
    pessoaService.inserir(new Pessoa(null));

}

By creating this test, imagining that you are starting practically from scratch, you will come across several errors:

  • Staff class does not exist
  • method insert into Personal Service does not exist
  • class Person does not exist
  • person class constructor does not exist to receive the name
  • Nameaninvalidoexception does not exist

At this point, you need to resolve all these issues to avoid compilation errors. The idea now is only to solve these compilation problems without implementation of logic (yet).

Once this is done, you will be able to run the test. The error will give an error as the Exception NomePessoaInvalidoException was not returned.

Thus, the implementation begins. You will need to read the person’s name (creating the name attribute within the person) and check if it is valid (not null):

@Service
class PessoaService {

    public Pessoa inserir(Pessoa pessoa) {

        if (pessoa.getNome() == null) {
            throw new NomePessoaInvalidoException("Nome está nulo");
        }

    }
}

After this, the test will pass. The other cases are similar, the main difference is if you need to find a person, you will need to mock the call that makes the search in the database.

In the case of naoInserirPessoaSePessoaExiste, you would have a mock test Pessoarepository to return a person to you. The test code would look like this:

@Test(expect = PessoaJaExisteException.class)
public void naoInserirPessoaSePessoaExiste() {

    PessoaRepository pessoaRepository = mock(PessoaRepository.class);
    PessoaService pessoaService = new PessoaService(pessoaRepository);
    pessoaService.inserir(new Pessoa("Eduardo"));

}

When running would give error. To work, the PessoaService would look something like:

public Pessoa inserir(Pessoa pessoa) {

    if (pessoa.getNome() == null) {
        throw new NomePessoaInvalidoException("Nome está nulo");
    }

    if (this.pessoaRepository.countByName("Eduardo").isPresent()) {
        throw new PessoaJaExisteException("Eduardo já existe");
    }

}

The success case follows the same logic. You could make a assertNotNull for the return of the method inserir, then implementing in the Service the call to the method save from the repository. I hope it was clear.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.