DEV Community

Cover image for Example of role tests in Java with Junit
Manuel Rivero
Manuel Rivero

Posted on • Originally published at codesai.com

Example of role tests in Java with Junit

I’d like to continue with the topic of role tests that we wrote about in a previous post, by showing an example of how it can be applied in Java to reduce duplication in your tests.

This example comes from a deliberate practice session I did recently with some people from Women Tech Makers Barcelona with whom I’m doing Codesai’s Practice Program in Java twice a month.

Making additional changes to the code that resulted from solving the Bank Kata we wrote the following tests to develop two different implementations of the TransactionsRepository port: the InMemoryTransactionsRepository and the FileTransactionsRepository.

These are their tests, respectively:

package bank.tests.integration;

// some ommited imports...

public class InMemoryTransactionsRepositoryTest {

  private TransactionsRepository repository;
  private List<Transaction> initialTransactions;

  @Before
  public void setup() throws ParseException {
    repository = new InMemoryTransactionsRepository();
    initialTransactions = Arrays.asList(
        aTransaction().withDeposit(100).on("10/10/2021").build(),
        aTransaction().withWithdrawal(50).on("15/10/2021").build());
    prepareData(initialTransactions);
  }

  @Test
  public void a_transaction_can_be_saved() throws ParseException {
    Transaction transaction = aTransaction().withDeposit(500).on("25/10/2021").build();

    repository.save(transaction);

    assertThat(repository.retrieveAll(), is(addTo(initialTransactions, transaction)));
  }

  @Test
  public void transactions_can_be_retrieved() {
    assertThat(repository.retrieveAll(), is(initialTransactions));
  }

  private void prepareData(List<Transaction> transactions) {
    for (Transaction transaction : transactions) {
      repository.save(transaction);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
package bank.tests.integration;

// some ommited imports...

public class FileTransactionsRepositoryTest {
  private static final String TRANSACTIONS_FILE = "src/test/resources/initial_transactions.txt";

  private TransactionsFile transactionsFile;
  private TransactionsRepository repository;
  private List<Transaction> initialTransactions;

  @Before
  public void setup() throws ParseException {
    transactionsFile = new TransactionsFile(TRANSACTIONS_FILE);
    repository = new FileTransactionsRepository(TRANSACTIONS_FILE);
    initialTransactions = Arrays.asList(
        aTransaction().withDeposit(100).on("10/10/2021").build(),
        aTransaction().withWithdrawal(50).on("15/10/2021").build());
    prepareData(initialTransactions);
  }

  @Test
  public void a_transaction_can_be_saved() throws ParseException {
    Transaction transaction = aTransaction().withDeposit(500).on("25/10/2022").build();

    repository.save(transaction);

    assertThat(repository.retrieveAll(), is(addTo(initialTransactions, transaction)));
  }

  @Test
  public void transactions_can_be_retrieved() {
    assertThat(repository.retrieveAll(), is(initialTransactions));
  }

  private void prepareData(List<Transaction> transactions) {
    try {
      transactionsFile.clean();
      transactionsFile.append(transactions);
    } catch (IOException e) {
      System.err.println("Error preparing transaction data in tests: " + e);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see both tests contain the same test cases: a_transaction_can_be_saved and transactions_can_be_retrieved but their implementations are different for each class. This makes sense because both implementations implement the same role, (see our previous post to learn how this relates to Liskov Substitution Principle).

We can make this fact more explicit by using role tests. In this case, Junit does not have something equivalent or similar to the RSpec’s shared examples functionality we used in our previous example in Ruby. Nonetheless, we can apply the Template Method pattern to write the role test, so that we remove the duplication, and more importantly make the contract we are implementing more explicit.

To do that we created an abstract class, TransactionsRepositoryRoleTest. This class contains the tests cases that document the role and protect its contract (a_transaction_can_be_saved and transactions_can_be_retrieved) and defines hooks for the operations that will vary in the different implementations of this integration test
(prepareData, readAllTransactions and createRepository):

package bank.tests.integration.roles;

// some ommited imports...

public abstract class TransactionsRepositoryRoleTest {
  protected TransactionsRepository repository;
  private List<Transaction> initialTransactions;

  @Before
  public void setup() throws ParseException {
    repository = createRepository();
    initialTransactions = Arrays.asList(
        aTransaction().withDeposit(100).on("10/10/2021").build(),
        aTransaction().withWithdrawal(50).on("15/10/2021").build());
    prepareData(initialTransactions);
  }

  @Test
  public void a_transaction_can_be_saved() throws ParseException {
    Transaction transaction = aTransaction().withDeposit(500).on("25/10/2021").build();

    repository.save(transaction);

    assertThat(readAllTransactions(), is(add(initialTransactions, transaction)));
  }

  @Test
  public void transactions_can_be_retrieved() {
    assertThat(repository.retrieveAll(), is(initialTransactions));
  }

  protected abstract TransactionsRepository createRepository();

  protected abstract List<Transaction> readAllTransactions();

  protected abstract void prepareData(List<Transaction> transactions);

  private List<Transaction> add(List<Transaction> transactions, Transaction transaction) {
    List<Transaction> result = new ArrayList<>(transactions);
    result.add(transaction);
    return result;
  }
}
Enter fullscreen mode Exit fullscreen mode

Then we made the previous tests extend TransactionsRepositoryRoleTest and implemented the hooks.

This is the new code of InMemoryTransactionsRepositoryTest:

package bank.tests.integration;

// some ommited imports...

public class InMemoryTransactionsRepositoryTest extends TransactionsRepositoryRoleTest {

  @Override
  protected TransactionsRepository createRepository() {
    return new InMemoryTransactionsRepository();
  }

  @Override
  protected void prepareData(List<Transaction> initialTransactions) {
    for (Transaction transaction : initialTransactions) {
      repository.save(transaction);
    }
  }

  @Override
  protected List<Transaction> readAllTransactions() {
    return repository.retrieveAll();
  }
}
Enter fullscreen mode Exit fullscreen mode

And this is the new code of FileTransactionsRepositoryTest after the refactoring:

package bank.tests.integration;

// some ommited imports...

public class FileTransactionsRepositoryTest extends TransactionsRepositoryRoleTest {
  private static final String TRANSACTIONS_FILE = "src/test/resources/initial_transactions.txt";
  private final TransactionsFile transactionsFile;

  public FileTransactionsRepositoryTest() {
    transactionsFile = new TransactionsFile(TRANSACTIONS_FILE);
  }

  @Override
  protected TransactionsRepository createRepository() {
    return new FileTransactionsRepository(TRANSACTIONS_FILE);
  }

  @Override
  protected void prepareData(List<Transaction> transactions) {
    try {
      transactionsFile.clean();
      transactionsFile.append(transactions);
    } catch (IOException e) {
      System.err.println("Error preparing transaction data in tests: " + e);
    }
  }

  @Override
  protected List<Transaction> readAllTransactions() {
    List<Transaction> transactions = new ArrayList<>();
    try {
      transactions = transactionsFile.readTransactions();
    } catch (IOException | ParseException e) {
      System.err.println("Error reading transaction data in test: " + e);
    }
    return transactions;
  }
}
Enter fullscreen mode Exit fullscreen mode

This new version of the tests not only reduces duplication, but also makes explicit and protects the behaviour of the TransactionsRepository role. It also makes less error prone the process of adding a new implementation of TransactionsRepository because just by extending the TransactionsRepositoryRoleTest, you’d get a checklist of the behaviour you need to implement to ensure substitutability, i.e., to ensure the Liskov Substitution Principle is not violated.

Have a look at this Jason Gorman’s repository to see another example that applies the same technique.

In a future post we’ll show how we can do the same in JavaScript using Jest.

Acknowledgements.

I’d like to thank the WTM study group, and especially Inma Navas and Laura del Toro for practising with this kata together.

Thanks to my Codesai colleagues, Inma Navas and Laura del Toro for reading the initial drafts and giving me feedback, and to Esranur Kalay for the picture.

References.

Photo from Esranur Kalay
in Pexels

Top comments (0)