Monday, February 13, 2017

Unit testing (with JUnit) and some emphasis on mock objects

A note ahead. This post is not about how JUnit works, so do not expect to get any detailed knowledge of JUnit itself. It's more about why we should do unit testing, what it is, what it is not. And we'll also try to shed some light on mock objects. 


Unit testing has long been a standard in software development. It's an integral part of any serious, professionally developed project. Just try to download any widely accepted project from GitHub and you'll most probably see that it's got its separate test folder that contains more or less the same number of unit test files as that for the real classes. So don't expect to be counted a serious developer if you plan to skip unit tests on your projects. Following are the main headlines that I'll be talking about in this post:

  1. What really unit testing is
  2. What unit testing is not
  3. What mock objects ARE, honestly

What is unit testing

Consider the following two classes:


public class Automobile {
    private Location currentLocation;

    public Location getCurrentLocation() {
        return currentLocation;
    }

    public void setCurrentLocation(Location location) {
        this.travelledSoFar += this.currentLocation.getDifferenceInMiles(location);
        this.currentLocation = location;
    }

    private float remainingFuel;

    public float getRemainingFuel() {
        return remainingFuel;
    }

    public void setRemainingFuel(float remainingFuel){
        if (remainingFuel > this.tankCapacity)
            this.remainingFuel = tankCapacity;
        else
            this.remainingFuel = remainingFuel;
    }

    private float travelledSoFar;

    public float getTravelledSoFar() {
        return travelledSoFar;
    }

    private final float tankCapacity;
    private final float mpg;

    public Automobile(float tankCapacity, float mpg) {
        this.tankCapacity = tankCapacity;
        this.mpg = mpg;
        this.currentLocation = new Location(0f, 0f);
    }

    public Automobile() {
        this(17f, 20f);
    }

    //Takes from current location to target location
    public void drive(Location target) {
        //What if we pass null for target? :)
        Location initialLocation = this.currentLocation;
        this.currentLocation = target;
        float distanceTravelled = this.currentLocation.getDifferenceInMiles(initialLocation);
        this.travelledSoFar += distanceTravelled;
        reduceRemainingFuel(distanceTravelled);
    }

    //Refuels with the supplied gallons of fuel
    public void refuel(float gallons) throws FuelAmountExceedingTankCapacityException {
        float amountAfterRefuel = this.remainingFuel + gallons;
        if (amountAfterRefuel > this.tankCapacity)
            throw new FuelAmountExceedingTankCapacityException(this.tankCapacity, amountAfterRefuel);
        this.remainingFuel += gallons;
    }

    private void reduceRemainingFuel(float travelledDistance) {
        this.remainingFuel -= travelledDistance / this.mpg;
    }
}


class Location {
    private float x;

    private float y;

    public float getX() {
        return this.x;
    }

    public void setX(float x) {
        this.x = x;
    }

    public float getY() {
        return y;
    }

    public void setY(float y) {
        this.y = y;
    }

    public float getDifferenceInMiles(Location anotherLocation) {
        return Math.abs(this.x - anotherLocation.x);
    }

    public Location(float x, float y) {
        this.x = x;
        this.y = y;
    }
}

The unit we're interested in testing is Automobile class. It's got some functionalities. We need to make sure that all the expectations from an Automobile are met. Ideally and in fact normally we should test all the public methods from as many different aspects as possible. So we start to ask these questions:

    The method void drive(Location targetLocation)
  • When we are at location A and want to drive to location B, does it really end up in B?
  • Does it update the mileage as it drives?
  • What if we say drive but don't specify any target location? Does it reset the mileage to 0?
  • Does the remaining fuel reduce during the trip?
    The method void refuel(float gallons)
  • Does it refuel exactly the same amount?
  • Is it protected against overfilling?

In fact, there are so many more questions to ask, like "What if you don't have enough fuel for that trip?" but for the sake of brevity, I'll skip many of them. So with these questions in mind, we go and set up our unit tests that will check if those intended behaviors are demonstrated by that unit. 

BE PATIENT AND DO NOT WORRY. I WILL EXPLAIN THE BENEFITS OF UNIT TESTING. I STILL REMEMBER THAT IT'S NOT A POST ABOUT HOW UNIT TESTING IS DONE IN JUNIT AND THAT I HAVE TO EXPLAIN WHY WE SHOULD DO UNIT TESTING. SO BEAR WITH ME.


import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;

/**
 * Class for unit testing Automobile class
 */
public class AutomobileTest {
    Location target;
    Automobile auto;

    @Before
    public void setUp() {
        target = new Location(100, 0);
        auto = new Automobile(20, 21);
    }

    @Test
    public void currentLocationIsCorrectAfterDrive() {
        auto.drive(target);
        assertTrue(auto.getCurrentLocation().getX() == 100);
    }
    @Test
    public void distanceTravelledSoFarEquals170() {
        Location location=new Location(80,40);
        auto.setCurrentLocation(location);
        auto.drive(target);
        target=new Location(30,70);
        auto.drive(target);
        assertTrue(auto.getTravelledSoFar()==170);
    }

    @Test
    public void remainsFiveGallonsOfFuelAfterTravelling210Miles()throws FuelAmountExceedingTankCapacityException{
        auto.setRemainingFuel(15);
        target.setX(210);
        auto.drive(target);
        //Mpg is 21, so after driving 250 miles, the tank should have 15-210/21=5.0f
        assertTrue(auto.getRemainingFuel()==5f);

    }
}

Here we've taken the drive method and set up some tests on its behavior. First, we wanted to see if it really drives to the given location. So we invoked that method and checked the location of the car after the ride. Then there's a second test that checks if the odometer works. For that, we prepared the car and target location with some test values and checked the odometer. And then we set up yet another test to see if (correct amount of) fuel is used, again by filling up the tank, driving and looking into the tank. We'll get this beautiful, all-green output if we run all tests:






Now you might ask why we need unit test when we can test any behavior by simply calling any method we're interested in with some test values and assert the correctness. Well, that's right and with a unit testing frameworks you do exactly the same but

  1. Although you can have your unit tests mixed with your source code, they're normally kept separate. So you don't have to clean the mess left after each time you want to make sure your code works as expected. Just think about how many classes and methods you can have in an average program. 
  2. Unit tests can be automated. You once set them up and make sure they're run during project builds. Do not confuse unit testing with compilation. It does not check whether your code is valid or not. It checks whether your code behaves as you want it to. And running your units is the only way to assert that. It's like a team of testers that test your code manually right after compilation by providing some probe values and calling your methods in many different contexts. They automatically do all the tests at the point you ask them to. Again, unit testing is not a compilation level test, it's a series of ordinary, predefined methods that eventually invoke methods on units(usually classes) with some test values or conditions. 
  3. They never miss anything. They make sure that all the behavior tests are conducted. But we are human beings and are prone to forget. We might change the way a method in one unit works, which can affect the behavior of another unit but forget to test the dependent unit. I'll expand on this a minute later
  4. Well, if you still say you can make your own unit testing framework, then go ahead.  
Now, let's take a look at the following method in the Location class:


    public float getDifferenceInMiles(Location anotherLocation) {
        return Math.abs(this.x - anotherLocation.x);
    }

It simply returns the absolute value of the difference between the x values of itself and the location passed. What if change the body of the method to this?:


    public float getDifferenceInMiles(Location anotherLocation) {
        return (float) Math.abs(Math.hypot(this.x - anotherLocation.x,
                                                         this.y-anotherLocation.y));

Well, the compilation will still succeed. But wait a minute. Our Automobile class seems to heavily depend on this method. Without unit testing in place, we can easily forget that the behavior of our drive method will change and won't work as expected. But with unit testing, we'll get a failure on the test named distanceTravelledSoFarEquals170. Here's the output if we run the test this way.






With unit testing, you make sure that the behaviors you've set up a test for are guarded against any change. Not that they will recover from the situation, but they will inform you that something's wrong with the way your method should work. Unit testing frameworks, in our case Junit, will give a list of passed and failed tests so you can concentrate on the failed ones.

Besides all these benefits, unit testing perfectly describes what your unit is built to do. So anyone, including yourself, looking at the test cases will understand the purpose of the unit better. 

And one more fantastic benefit: It'll force you to design your API better by making you abstract it away. You'll notice that your code becomes more cohesive and decoupled as you try to make it better testable. It turns out unit testing has brought lots of good side effects although the main intention was to test the correctness of behavior.


What unit testing is not


  • It's not integration test.As the name implies, unit testing is to test a unit of work at its own level. Integration test is the process where you test whether different components work together well. 
  • It's not meant for testing other people's code. You test how your piece of code behaves under certain circumstances. In fact, you can conclude that some other dependency that your unit depends on is buggy by testing your own code. Honestly, what can you do if Math.hypot method does not work right?
  • Unit testing is not meant to replace the process of testing your software as a whole project. You still need to run it and see if it works as expected. And it's definitely not to replace the functional testing phase. 
  • It's not a waste of time. Despite the fact that you spend a decent amount of time on writing unit tests, that pays off very quickly. Think about a car manufacturing plant. Before production, you build the automation process. That takes some time. But then you sit and relax while the robots build cars. I know it's not exactly the same but the idea behind time spending is the same.

What are mock objects, honestly

As I mentioned earlier, unit testing is focused on a unit of work. We just test one functionality by creating test conditions. But sometimes we depend on other objects that affect how our code works. That dependency might behave differently at various circumstances and we need to make sure that our System Under Test (SUT, that's one of the several ways of calling our unit that we want to test) acts accordingly. Mocks are one of the ways of getting the dependency to do what you want. They are just skeletons of the classes. They have no functionality. Let's say your automobile class depends on another class called GpsNavigator. You don't always get the same route to a location. The result depends on many factors like traffic, weather etc. So to be able to see if the automobile drives as expected, we need to make the GPS navigator do and return just want we want it to. Assume the GpsNavigator class looks something like this:

    public class GpsNavigator{
      public Route getRouteForAddress(String address){
         return doSomeMagicAndGetMeTheRouteToAddress(address);
      }
    }

If that doSomeMagicAndGetMeAddress method is not guaranteed to return the same value for the same input each time you invoke it, then you won't be able to build your assumptions on that. For that same reason, we need to be able to control this. Take a look at the below code. We use Mockito for this post.


   //some other imports
   ...
   import static org.mockito.Mockito.*;
   public class AutomobileTest{
     final GpsNavigator gpsMock = mock(GpsNavigator.class);
     Automobile auto=new Automibile(gpsMock);
     
     @Test
      public void remainingFuelIs5GallonsAfterDrivingFromZeroToCupertiono(){
          Route routeThatNeeds10GallonsOfFuel=RouteHelper.getRouteForGallons(10);         
          when(gpsMock.getRouteForAddress("Cupertino")).thenReturn(routeThatNeeds10GallonsOfFuel);
          auto.setRemaningFuel=15f;
          auto.driveByGps("Cupertino");
          assertTrue(auto.getRemainingFuel.equals(5f));
      }
     
   }

  1. We created a mock object of GpsNavigator class.
  2. We passed that object to our SUT, the automobile class via its constructor. We have to design our automobile class so that we can inject its dependencies externally. Constructors are one way of doing this. We do this because we want to make sure that during the test the automobile class uses our mock object, instead of a real one.
  3. Then inside the test method, we instruct our mock object to always return the particular Route (in this case the one that needs 10 gallons of fuel) whenever we need to go to Cupertino.
  4. We make sure that we have 15 gallons of fuel in the tank
  5. We drive to Cupertino
  6. Finally, we assert that the remaining amount of fuel is 5 gallons
Now, what do you think the gpsMock object would return if we omitted this line?

when(gpsMock.getRouteForAddress("Cupertino")).thenReturn(routeThatNeeds10GallonsOfFuel);

It would return null. Why? Because method bodies of mock objects are empty. To be more precise, their bodies either contain nothing between two curly braces or return the default value of the return type of the method. In other words, they lack the real implementation. This is how the bodies of the methods in the GpsNavigator class looks like when we mock it:


public class GpsNavigator{
   public Route getRouteForAddress(String){
     return null;
   }
   public boolean hasArrivedToDestination(){
     return false;
   }
   private void calculateTheMostEfficientRoute(){
     //nothing here :)
   }  
}



How Mockito does this is beyond the scope of this post. If you're really interested, you can learn about CGLIB or you can go directly to the Mockito source.






No comments:

Post a Comment