Some ideas about Unit Testing

Introduction

In our previous tutorial, we created a complete flow to handle a single entity. Now, we want to introduce the concept of unit testing in our application.

In software development, there are various approaches to testing, such as unit testing, integration testing, functional testing, end-to-end testing, and more. In this tutorial, we will focus on creating some simple unit tests. Later, we can explore other types of testing using specialized frameworks.

Testing lemonade stand application

What do we create?

Today, we want to exemplify what is called white-box testing. This means we have access to our existing code, written previously, and now we will write unit tests for it.

The concept of unit testing is essential in software development for several reasons. Below are some benefits of writing unit tests in Java, though there are many more:

Ensures Code Reliability

Unit tests allow you to validate that the individual units (such as methods or classes) of your code work as expected. By catching bugs early in the development process, you reduce the risk of issues occurring in production. 

 Facilitates Refactoring

When refactoring code (i.e., restructuring existing code without changing its external behavior), unit tests act as a safety net. They verify that your changes haven’t broken any functionality, ensuring that the code still works as expected after modifications.

Improves Code Quality

Writing tests often forces developers to write more modular, loosely coupled code. This is because code that is easy to test typically adheres to good design principles like Single Responsibility Principle.

Each method used in our program (except UI methods) should be tested. For now, we will create tests for our existing application. Moving forward, we will add tests whenever we introduce new features.

Each time the program is started, all the tests should run to ensure that our changes do not negatively impact the rest of the application as new features are added.

How do we create it?

When writing tests, there are several good practices to follow. We will discuss some of these with examples.

First, we will create a new package for the new test classes. For each layer, we will create a corresponding test class. Additionally, we will create a main class to run all the tests together. The exception is the UI layer, which cannot be tested because it requires user interaction.

We should replicate the package structure of our existing application for the test classes, but this time, in the src/main/test directory. The structure of the test directory should mirror that of the application:

Multi layer application with tests structure

Each class in our application should have a corresponding class in the testing package. For instance, in the Domain folder, we should create a new class: SupplierTest.

A good practice is to create one method for each function or behavior. But take care of Single Responsibility Principle: each method should have a single test case scenario.

The name of the methods should be meaningful and easy to read. We can use the following convention: shouldDoX_whenY

				
					 public void shouldGetCorrectValues_whenConstructorIsCalled(){
        Supplier supplier = new Supplier(1, "Lemonades","contact@lemonades.com");

        assert supplier.getId() == 1;
        assert supplier.getName().equals("Lemonades");
        assert supplier.getContactEmail().equals("contact@lemonades.com");
    }
				
			
				
					    public void shouldSetCorrectValues_whenSettersAreUsed(){
        Supplier supplier = new Supplier(1, "Lemonades","contact@lemonades.com");

        supplier.setId(2);
        supplier.setName("Lemonades name");
        supplier.setContactEmail("contactlemonades@lemonades.com");

        assert supplier.getId() == 1;
        assert supplier.getName().equals("Lemonades name");
        assert supplier.getContactEmail().equals("contactlemonades@lemonades.com");
    }
				
			
				
					public class SupplierTest {


    public void shouldGetCorrectValues_whenConstructorIsCalled(){
        Supplier supplier = new Supplier(1, "Lemonades","contact@lemonades.com");

        assert supplier.getId() == 1;
        assert supplier.getName().equals("Lemonades");
        assert supplier.getContactEmail().equals("contact@lemonades.com");
    }

    public void shouldSetCorrectValues_whenSettersAreUsed(){
        Supplier supplier = new Supplier(1, "Lemonades","contact@lemonades.com");

        supplier.setId(2);
        supplier.setName("Lemonades name");
        supplier.setContactEmail("contactlemonades@lemonades.com");

        assert supplier.getId() == 2;
        assert supplier.getName().equals("Lemonades name");
        assert supplier.getContactEmail().equals("contactlemonades@lemonades.com");
    }

    public void testAllDomain() {
        shouldGetCorrectValues_whenConstructorIsCalled();
        shouldSetCorrectValues_whenSettersAreUsed();
    }
    
}

				
			

When we are creating tests, we should follow the Arrange-Act-Assert (AAA) Pattern. This pattern improves readability and structure by clearly separating the setup, action, and verification steps of the test.

  • Arrange: Set up the necessary objects and inputs.
  • Act: Perform the action being tested.
  • Assert: Verify the expected outcome

An example should be observed bellow, for the SupplierRepositoryTest class.

				
					  public void shouldSaveOneElement_whenSaveIsCalled(){
            // arrange
            SupplierRepository supplierRepository = new SupplierRepository();
            Supplier firstSupplierToSave = new Supplier(1, "Lemonades", "contact@lemonades.com");
            
            // act
            Supplier firstSavedSupplier = supplierRepository.save(firstSupplierToSave);

            
            //assert
            assert firstSavedSupplier != null;
            assert firstSavedSupplier.getId() == 1;
            assert firstSavedSupplier.getName().equals("Lemonades");
            assert supplierRepository.findById(2) == null;
    }
				
			

For the same method we should have different scenarios, but we must test one thing at a time:

				
					 public void shouldSaveTwoElements_whenSaveIsCalledTwice(){
        SupplierRepository supplierRepository = new SupplierRepository();

        Supplier firstSupplierToSave = new Supplier(1, "Lemonades", "contact@lemonades.com");
        Supplier firstSavedSupplier = supplierRepository.save(firstSupplierToSave);

        Supplier secondSupplierToSave = new Supplier(2, "Water", "contact@water.com");
        Supplier secondSavedSupplier = supplierRepository.save(secondSupplierToSave);

        assert firstSavedSupplier.getId() == 1;
        assert firstSavedSupplier.getName().equals("Lemonades");
        assert supplierRepository.findById(3) == null;

        assert secondSavedSupplier != null;
        assert secondSavedSupplier.getId() == 2;
        assert secondSavedSupplier.getName().equals("Water");
        
        assert supplierRepository.findById(1) != null;
        assert supplierRepository.findById(2) != null;
        
    }
				
			
				
					public class SupplierRepositoryTest {

        public void shouldSaveOneElement_whenSaveIsCalled(){
            SupplierRepository supplierRepository = new SupplierRepository();
            Supplier firstSupplierToSave = new Supplier(1, "Lemonades", "contact@lemonades.com");
            
            Supplier firstSavedSupplier = supplierRepository.save(firstSupplierToSave);
            
            assert firstSavedSupplier != null;
            assert firstSavedSupplier.getId() == 1;
            assert firstSavedSupplier.getName().equals("Lemonades");
            assert supplierRepository.findById(2) == null;
        }

    public void shouldSaveTwoElements_whenSaveIsCalledTwice(){
        SupplierRepository supplierRepository = new SupplierRepository();

        Supplier firstSupplierToSave = new Supplier(1, "Lemonades", "contact@lemonades.com");
        Supplier firstSavedSupplier = supplierRepository.save(firstSupplierToSave);

        Supplier secondSupplierToSave = new Supplier(2, "Water", "contact@water.com");
        Supplier secondSavedSupplier = supplierRepository.save(secondSupplierToSave);

        assert firstSavedSupplier.getId() == 1;
        assert firstSavedSupplier.getName().equals("Lemonades");
        assert supplierRepository.findById(3) == null;

        assert secondSavedSupplier != null;
        assert secondSavedSupplier.getId() == 2;
        assert secondSavedSupplier.getName().equals("Water");

        assert supplierRepository.findById(1) != null;
        assert supplierRepository.findById(2) != null;

    }

        public void shouldUpdateSupplier_whenUpdateMethodCalled(){
            SupplierRepository supplierRepository = new SupplierRepository();

            Supplier supplierToUpdate = new Supplier(1, "Lemonades", "contact@lemonades.com");
            supplierRepository.save(supplierToUpdate);

            supplierToUpdate.setName("Burger");
            supplierToUpdate.setContactEmail("contact@burgers.com");

            Supplier updatedSupplier = supplierRepository.update(supplierToUpdate);
            assert updatedSupplier != null;
            assert updatedSupplier.getId() == 1;
            assert updatedSupplier.getName().equals("Burger");
            assert updatedSupplier.getContactEmail().equals("contact@burgers.com");

        }

        public void shouldDeleteSupplier_whenDeletedMethodIsCalled(){
            SupplierRepository supplierRepository = new SupplierRepository();

            Supplier supplierToDelete = new Supplier(1, "Lemonades", "contact@lemonades.com");
            supplierRepository.save(supplierToDelete);

            supplierRepository.delete(1);

            Supplier deletedSupplier = supplierRepository.findById(1);

            assert deletedSupplier == null;
        }

        public void shouldFindSupplier_whenFindMethodCalled(){
            SupplierRepository supplierRepository = new SupplierRepository();

            Supplier firstSupplierToSave = new Supplier(1, "Lemonades", "contact@lemonades.com");
            supplierRepository.save(firstSupplierToSave);
            Supplier secondSupplierToSave = new Supplier(2, "Water", "contact@water.com");
            supplierRepository.save(secondSupplierToSave);


            Supplier firstSupplier = supplierRepository.findById(1);
            Supplier secondSupplier = supplierRepository.findById(2);

            assert firstSupplier.getId() == 1;
            assert secondSupplier.getId() == 2;
        }

        public void testAllRepository() {
            shouldSaveOneElement_whenSaveIsCalled();
            shouldSaveTwoElements_whenSaveIsCalledTwice();
            shouldUpdateSupplier_whenUpdateMethodCalled();
            shouldDeleteSupplier_whenDeletedMethodIsCalled();
            shouldFindSupplier_whenFindMethodCalled();
        }

}

				
			

When we have some attributes that are created in multiple test methods, we can create a method for instancing before each test to avoid code repetition. Let’s see how that works for SupplierServiceTest class:

				
					 private void setUp(){
    SupplierRepository supplierRepository = new SupplierRepository();
    supplierService = new SupplierService(supplierRepository);
}

public void shouldSaveSupplier_whenSavedMethodCalled() {
    setUp();

    Supplier savedSupplier = supplierService.saveSupplier(1, "Lemonades", "contact@lemnodes.com");
    assert savedSupplier != null;
    assert savedSupplier.getId() == 1;
    assert savedSupplier.getName().equals("Lemonades");
    assert supplierService.findById(1).getId() == 1;
}
				
			
				
					public class SupplierServiceTest {

    private SupplierService supplierService;

    private void setUp(){
        SupplierRepository supplierRepository = new SupplierRepository();
        supplierService = new SupplierService(supplierRepository);
    }


    public void shouldSaveSupplier_whenSavedMethodCalled() {
        setUp();

        Supplier savedSupplier = supplierService.saveSupplier(1, "Lemonades", "contact@lemnodes.com");

        assert savedSupplier != null;
        assert savedSupplier.getId() == 1;
        assert savedSupplier.getName().equals("Lemonades");
        assert supplierService.findById(1).getId() == 1;
    }
    
    public void shouldUpdateSupplier_whenUpdateMethodCalled() {
        setUp();

        Supplier savedSupplier = supplierService.saveSupplier(1, "Lemonades", "contact@lemnodes.com");
        Supplier updatedSupplier = supplierService.updateSupplier(1, "Burger", "contact@burgers.com");

        assert updatedSupplier != null;
        assert updatedSupplier.getId() == 1;
        assert updatedSupplier.getName().equals("Burger");
        assert updatedSupplier.getContactEmail().equals("contact@burgers.com");
    }

    public void shouldRemoveSupplier_whenRemoveMethodCalled() {
        setUp();

        Supplier savedSupplier = supplierService.saveSupplier(1, "Lemonades", "contact@lemnodes.com");
        supplierService.removeSupplier(1);

        Supplier deletedSupplier = supplierService.findById(1);
        assert deletedSupplier == null;
    }

    public void shouldFindSupplier_whenFindMethodCalled() {
        setUp();

        supplierService.saveSupplier(1, "Lemonades", "contact@lemonades.com");
        supplierService.saveSupplier(2, "Water", "contact@water.com");


        Supplier firstSupplier = supplierService.findById(1);
        Supplier secondSupplier = supplierService.findById(2);

        assert firstSupplier.getId() == 1;
        assert secondSupplier.getId() == 2;
    }


    public void testAllService() {
        shouldSaveSupplier_whenSavedMethodCalled();
        shouldUpdateSupplier_whenUpdateMethodCalled();
        shouldRemoveSupplier_whenRemoveMethodCalled();
        shouldFindSupplier_whenFindMethodCalled();
    }

}
				
			

At the end, we should run all the test classes. For now, we will do this manually, but in the future, we will have a special framework which will work for us.

				
					public class MainTest {

    public void runAllTest(){

        SupplierTest domainTests = new SupplierTest();
        domainTests.testAllSupplier();

        SupplierRepositoryTest repositoryTests = new SupplierRepositoryTest();
        repositoryTests.testAllSupplierRepository();

        SupplierServiceTest serviceTests = new SupplierServiceTest();
        serviceTests.testAllSupplierService();

        System.out.println("All tests have run successfully!");
    }
}
				
			

And the method should be used in Main, before we start the application:

				
					 public static void main(String[] args){
        SupplierRepository supplierRepository = new SupplierRepository();
        SupplierService supplierService = new SupplierService(supplierRepository);
         UserInterface userInterface = new UserInterface(supplierService);

        MainTests tests = new MainTests();
        tests.runAllTest();


        userInterface.runMenu();
    }
				
			

Key-points

  • testing is an important step in writing code
  • there are many types of testing in software development 
  • unit tests are automated tests that validate the functionality of individual components or units of code, typically functions or methods
  • the concept of unit testing helps us to catch bugs early, improve code quality, facilitate easier refactoring, and increase confidence in changing code

Conclusion

In this tutorial, we’ve created our first tests. It is important to understand that there are different ways to test software. We’ve introduced the basics of unit testing using simple asserts. From now on, after each functionality added, we should add tests as well.

In the next tutorial, we will explore exceptions for error handling.