metrangia
metrangia

Reputation: 137

How can I use Mockito to eliminate an external dependency?

I'm having some trouble grasping the concept of Mockito. I've written a small program to help, but I can't get it to do what I want.

Here is my code:

// WeatherDemo.java:

package com.abc;

public class WeatherDemo {
    public String getForecast() {
        // Get the high remperature for today, and return back to the caller one of these values:
        // cold, mild, or hot
        // cold will be returned if the high temp is forecast to be less than 60.
        // hot will be returned if the high temp is forecast to be more than 79.
        // Otherwise, mild will be returned (this indicates a high temp in the 60s or 70s).
        int highTemp = getHighTemp();
        if (highTemp < 60)
            return("cold");
        if (highTemp > 79)
            return("hot");
        return("mild");
    }

    public int getHighTemp() {
        // Because this is a demo, we don't have access to any source (web service, DB, etc.) to get the high temp.
        // Just hard code a value here, but remember that if this were a real application, we would be dynamically
        //   retrieving the day's high temperature from some external source.
        int highTemp = 32;
        return(highTemp);
    }
}

================================================================================

// TestWeatherDemo.java:

package com.abc;

import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
import org.junit.Test;
import org.mockito.Mockito;

public class TestWeatherDemo {
    @Test
    public void testWeatherReport() {
        WeatherDemo testMockito = Mockito.mock(WeatherDemo.class);
        WeatherDemo testJUnit = new WeatherDemo();

        when(testMockito.getHighTemp()).thenReturn(90);
        assertEquals("hot", testJUnit.getForecast());
    }
}

================================================================================

Basically, I want to run a JUnit on getForecast(). It returns either cold, mild, or hot, depending on the high temperature for the day. To get the high temp, it calls getHighTemp(). Let's assume that getHighTemp() calls a web service to get the temperature (I hard coded a value just for test purposes). Because this is an external resource, my Junit doesn't pass the isolation test and isn't really a unit test at all. Not to mention the fact that getHighTemp() won't return the same value every time it is called.

Therefore, I want to mock getHighTemp(), telling it to always return a temp of 90.

The Mockito test is run from testWeatherReport(). This is where I am stuck. I can mock getHighTemp() to return 90 when doing this:

when(testMockito.getHighTemp()).thenReturn(90);

But, I am unable to get it to return 90 when called from getForecast(). The assert gets "cold" because it's picking up the 32, not the 90.

Isn't the whole idea behind Mockito that I can mock a method and tell it exactly what to return, in order to remove the external dependency? If calling getHighTemp() from getForecast() won't return 90, I don't see the purpose of Mockito. What am I missing here? Thanks for the help and enlightenment.

Bill

Upvotes: 2

Views: 1016

Answers (2)

sprinter
sprinter

Reputation: 27986

You are essentially asking 'how can I mock one method in my class while testing the rest'. It is possible using Mockito - search the documentation for 'partial mocks'. However it is (almost) always a sign that your code is badly structured and needs to be refactored. If you are testing a class that accesses an interface that you want to mock then that's a sign that you should declare the interface as an interface and then pass an implementation to the class. That has two effects: firstly it lets you change the implementation without changing the interface; secondly, it makes the class testable.

So in your case:

public interface TempSupplier {
    int getHighTemp();
    int getLowTemp();
}

public class WeatherDescriber {
    private final TempSupplier tempSupplier;

    public WeatherDescriber(TempSupplier tempSupplier) {
        this.tempSupplier = tempSupplier;
    }

    public String getForecast() {
        int highTemp = tempSupplier.getHighTemp();
        ...
    }
}

@Test
public void testForecast() {
    TempSupplier supplier = mock(TempSupplier.class);
    when(supplier.getHighTemp()).thenReturn(90);
    WeatherDescriber describer = new WeatherDescriber(supplier);
    assertThat(describer.getForecast(), is("Hot"));
}

I commonly split the mocking into a separate method so you can test easily:

private WeatherDescriber getDescriber(int lowTemp, int highTemp) {
    TempSupplier supplier = mock(TempSupplier.class);
    when(supplier.getLowTemp()).thenReturn(lowTemp);
    when(supplier.getHighTemp()).thenReturn(highTemp);
    return new WeatherDescriber(supplier);
}

@Test
public void testDescribeVariousTemps() {
    assertThat(getDescriber(10, 20).getForecast(), is("cold"));
    assertThat(getDescriber(30, 35).getForecast(), is("cold"));
    assertThat(getDescriber(40, 45).getForecast(), is("warmer"));
    assertThat(getDescriber(90, 130).getForecast(), is("melting"));
} 

Upvotes: 4

Matt Holland
Matt Holland

Reputation: 2210

In your example, testMockito and testJUnit are different objects - you've mocked the method on testMockito but testJUnit has your actual implementation of the method, returning the 32.

You may want to consider breaking up the code a little into two classes - you can then make the code in getHighTemp() a dependency of your main class. This is where you would supply the mock in testing and the real implementation for actually running the code. (Maybe by some kind of Dependency Injection)

The code being built in this way would seem to obey the Single Responsibility Principle - A benefit of this is that it makes it a little easier if you ever need to change your weather data supplier, for example. I think you're going along these lines anyway when you say "Because this is an external resource, my Junit doesn't pass the isolation test".

Upvotes: 1

Related Questions