Skip to main content

Unit Tests - Why are they so important?

Why do we need Unit Testing?
There are two important reason:
  • To test different scenarios and making sure the logic we have just written works as expected
  • In case a developer is changing the logic in future, they notice whether they have broken the existing logic or not

What Should Be Tested?
  • Different scenarios: E.g. each if-else statement can be taken as a separate scenario
  • Data that we expect to be changed within a method
  • Possible exceptions that may be thrown

What Does Not Need to Be Tested?
  • Mocked data that we created as input data
  • Any result from a third party API. E.g: Hibernate, Spring etc.
  • Results from classes that are already tested in their own unit tests.

Example



public Employer save(Employer employer) {

       if (employer.getActivateFlag() != null ) {
           if (employer.getActivateFlag()) {
               employer.setStatus(Status.Active);
           } else {
               employer.setStatus(Status.Inactive);
           }
       }

       employer = entityManager.save(employer);

       // Do something with the ID assigned to employer after save() is performed.

       if(employer.getEmployees() != null && employer.getEmployees().size() > 0){
           for (Employee employee : employer.getEmployees()) {
               Bonus bonus  = new Bonus();
               bonus.setEmployee(employee);
               bonus.setEmployer(employer);
               employee.addBonus(bonus);
           }
       }

       return entityManager.save(employer);

}
And here is the unit test for the above code. Please read the comments between the lines of the code:


@Test
public void testSave() {

    //Facts and inputs:


    Employer employer = new Employer();
    Set employees = new HashSet();
    Employee employee = new Employee();
    employee.setId(1);
    employees.add(employee);
    employer.setEmployerId(420);
    employer.setName("Name");
    employer.setActivateFlag(true);
    employer.setStatus(null); //Nulled out for the purpose of testing
    employer.setEmployees(employees);


    //Assumptions:
    when(employerDAO.entityManager.save(employer)).thenReturn(employer);
 

    //Testing scenarios:
 

    //Scenario 1 testing Active status

    Employer result = employerDAO.save(employer);
    verify(employerDAO.entityManager, new Times(2)).save(employer); //The save is called twice in save()
    assertEquals(result.getId(), new Integer(420)); //We need to make sure the returned Employer is the same as we expected nothing else. E.g. what if a developer mistakenly return a different Employer??

    assertEquals(result.getStatus(), Status.Active); // Expectation: Should be changed to Active


 

    //Scenario 2 testing Inactive status

    employer.setActivateFlag(false); // changing the fact to test the second scenario
    reset(employerDAO.entityManager); //Reset the counter on verify before calling the actual code
    when(employerDAO.entityManager.save(employer)).thenReturn(employer); //we need this because we reset the mock object
    result = employerDAO.save(employer);
    verify(employerDAO.entityManager, new Times(2)).save(employer);
    assertEquals(result.getId(), new Integer(420)); //Make sure it's the same Employer
    assertEquals(result.getStatus(), Status.Inactive); // Expectation: Should be changed to Inactive

 

    //Scenario 3 testing Bonus and making sure for-loop is executed correctly
    result = employerDAO.save(employer);
    assertNotNull(result.getEmployees());
    assertEquals(result.getEmployees().size(), 1);
    assertEquals(result.getEmployees().iterator().next().getBonus().getEmployee().getId(), new Integer(1)); // Expectation: Employee should contains bonus

 

}

COMMENTS:
  • It's up to you whether to separate scenarios in different unit test methods or just combine them into one method. However, you may find it easier for small blocks like save() method here to combine them as the second scenarios onward are just copy of the first/previous scenario with some modifications. Having said that, if the code we are testing is too big, it could be more efficient if we separate them into different unit test methods.
  • You might be thinking using verify() is pointless or no benefit. However, in future, if the logic is modified and moved into a loop involving another Employer for example, then verify assures that the one we are expecting is being saved.

How does the unit test above help?
Scenario 1
Now imagine, method save() incorrectly modified by another developer to the following code:


public Employer save(Employer employer) {
  

        // New business requirement is added  (1)

       Employer dependentEmployer = anotherDAO.findByPartyType(PartyTypeEnum.DependentParty);
       if(dependentEmployer.getBatchModeEnabledFlag()) {
           employer.setNotation("Batch mode enabled");
           employer.setBatchModeEnabledFlag(true);
       }

       if (employer.getActivateFlag() != null ) {
           if (employer.getActivateFlag()) {
               dependentEmployer.setStatus(Status.Active); //NOTE this is mistakenly set to 'dependentEmployer' (2)
           } else {
               employer.setStatus(Status.Inactive);
           }

       }

      //IGNORING THE REST OF THE CODE FOR SIMPLIFICATION

}


(1) The new requirement is added correctly
(2) Mistakenly dependentEmployer is being modified.
The unit test above will fail preventing further bugs and issues which are costly
Scenario 2
Now to show how verify() is beneficial, imagine this modification:

public Employer save(Employer employer) {
 

        // New business requirement is added - (1)

       Employer dependentEmployer = anotherDAO.findByPartyType(PartyTypeEnum.DependencyOne);
       if(dependentEmployer.getBatchModeEnabledFlag()) {
           employer.setNotation("Batch mode enabled");
           employer.setBatchModeEnabledFlag(true);
       }


       if (employer.getActivateFlag() != null ) {
           if (employer.getActivateFlag()) {
               employer.setStatus(Status.Active);
           } else {
               employer.setStatus(Status.Inactive);
           }

       }


    employer = entityManager.save(dependentEmployer); // Wrong save - (2)

       // Do something with the ID assigned to employer after save() is performed.

       if(employer.getEmployees() != null && employer.getEmployees().size() > 0){
          for (Employee employee : employer.getEmployees()) {
               Bonus bonus  = new Bonus();
               bonus.setEmployee(employee);
               bonus.setEmployer(employer);
               employee.addBonus(bonus);

           }

       }

  

    return entityManager.save(employer); // This fools the unit tests if there is no verify() - (3)
}



(1) New business requirement
(2) Mistakenly dependentEmployer is being saved
(3) This is correct but imagine if we didn't have verify() we wouldn't notice the dependentEmployer is also being saved so just having [when(employerDAO.entityManafer.save(employer)).thenReturn(employer)] is not enough in unit tests.
Without verify() the unit test we have created would be passed successfully ignoring the bug
 

Incorrect Unit Test Example
The following code is an incorrect unit test. This unit test will pass SUCCESSFULLY for both scenarios we mentioned above that produced a new bug:

@Test
public void testSave() throws Exception {

    Employer employer = new Employer();
    Set employee = new HashSet();

    Employee party = new Employee();
    party.setId(1);
    employee.add(party);
    employer.setEmployerId(420);
    employer.setName("Name");
    employer.setActivateFlag(true);
    employer.setStatus(null); //Nulled out for testing
    employer.setEmployees(employee);

    when(employerDAO.entityManager.save(employer)).thenReturn(employer); 

    Assert.assertNotNull("Id should be assigned for new record", employerDAO.save(entryType).getId());

}

Comments

Popular posts from this blog

CI/CD Automation: How to wait for Helm deployment into Kubernetes cluster

 So I created this post because with Helm v3 , if you use --wait option and the timeout is reached and the deployment isn't successful, Helm marks the deployment as failed . The problem is that subsequent deployment (upgrade) that may contain the fix won't work as expected and it will end up in error like this: [some-name] has no deployed release   The only way to fix this is manual intervene and deleting the previous failed release. This is not only scary but also against the automation process. If we remove --wait option, Helm will mark the deployment as successful regardless. My solution to this that works nicely is as per below: Remove --wait option from helm deploy Use this command to retrieve the list of deployment for that namespace that you are deploying against: kubectl get deployments -n ${namespace} -o jsonpath='{range .items[*].metadata}{.name}{","}{end}' You can use split to turn the comma separated list above into an array Then you can run mul...

Hibernate And Mapping enum to customized values

With Hibernate, enums can be easily mapped either by enum item name or the position of each item but what if you want to map it to a customized value? In my case, we have so many one-character long columns in our tables, representing flags, statuses etc. We have heaps of them. Writing UserTypes for each enum field is very boring and nasty job. As every where you see in internet, you need to keep a Map for each user-type in order to map those values to enum elements. So to avoid this, I ended up with something more clean, easy and more generic. Now imagine you have following enum: public enum PaymentFrequencyEnum { WEEKLY("WK"), FORTNIGHTLY("FN"), MONTHLY("MT"), QUARTERLY("QL"), YEARLY("YL"); private String value;     private PaymentFrequency(String value) { this.value = value; } } I've chosen two-letter code as value so that you understand m...

JSF or GWT?

I have worked with JSF for almost 4 years. First time when I had a chance to work with GWT, I was very excited. Like many Java developers I was like 'Wow I don't need to play with those bloody tags and elements and now it's pure Java code!!!'... I was so happy but my happiness didn't last very long. Programming is my passion. I hate writing codes that become a mess later and unfortunately this is what will eventually happen with GWT. The thing is you can't rely on reviews and features of a software or an API, in action everything goes different way. Specially when it comes to large scaled application and in an environement where everything is urgent and importnat... well, I think all software companies are the same in this regard... The fact is that in a team of developers not every one cares about best practices, design patterns and even if you have very experienced designer, solution architect etc, still you can't force the team to deve...