简体   繁体   中英

Catch unknown host exception in unit test

I am writing some unit tests and try to cover as much as posible from my code. Now I want to write a test that verifies the name of the local host.

The method looks like this:

public static String getLocalhostName() {
      try {
         return InetAddress.getLocalHost().getHostName();
      }
      catch ( final UnknownHostException e ) {
         throw new RuntimeException( e.getMessage() );
      }
   }

and my test:

@Test
public void testGetLocalhostName() {
  final String host = getLocalhostName();
  Assert.assertEquals( "mycomputer", host );
}

The question is how can I refactor this in order to cover also the catch block from the main method?

As a rule of thumb, you should not be mocking external libraries. By mocking, you make an assumption how it behaves, what may not be the case or what may change in future. You should only mock code that you control and code that has your tests.

In this case, I would extract InetAddress.getLocalHost().getHostName() to another class HostNameProvider and then do dependency injection, just like in John Williams' answer but without using power mock. Then you would have two tests for your current class and one for HostNameProvider (only happy path):

  • test 1: assert that class under test returns whatever HostNameProvider returns
  • test 2: assert that an exception is thrown when HostNameProvider throws an exception
  • HostNameProviderTest: assert that it returns something reasonable; it's more of an integration test as the returned value depends on where you run the test, it would likely return different values when run locally and when run in CI tool (like Jenkins or Bamboo)

However what happens in practice when applying test driven development? You write a test, you write implementation and it doesn't compile so you have to fix it by adding try-catch. At this point I would rely that no developer is so irresponsible to just swallow the exception. And if anyone does, you have static analysis - both IntelliJ and Sonar will highlight this. In short - I wouldn't test it.

I also recommend to modify the templates, so when you generate try-catch, it will automatically add throw new RuntimeException(e); in the catch.

@Test
public void testGetLocalhostName() {
    String url = "http://hostname/xxxxx";
    Throwable throwable = catchThrowable(() -> foo.getLocalHost(url));

    assertThat(throwable)
        .isNotNull()
        .isInstanceOf(RuntimeException.class)
        .hasMessage("your message");
    assertThat(throwable.getCause()).isInstanceOf(UnknownHostException.class);
}

You can test exception type, message and test hostname with this way.

This will get you your test coverage coverage.

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import java.net.InetAddress;
import java.net.UnknownHostException;

import static org.mockito.BDDMockito.given;


@RunWith(PowerMockRunner.class)
@PrepareForTest(InetAddress.class)
public class Mocker {

    @Mock
    private java.net.InetAddress mockedAddress;

    @Test(expected = RuntimeException.class)
    public void testGetLocalHostName() throws Exception {

        //given
        PowerMockito.mockStatic(InetAddress.class);
        given(InetAddress.getLocalHost()).willReturn(mockedAddress);
        given(mockedAddress.getHostName()).willThrow(UnknownHostException.class);

        //when
        getLocalhostName();
    }

    public static String getLocalhostName() {
        try {
            return InetAddress.getLocalHost().getHostName();
        } catch (final UnknownHostException e) {
            throw new RuntimeException(e.getMessage());
        }
    }

}

But it isn't satisfactory because it doesn't verify that the expected code has run before the RuntimeException is thrown.

Capturing the exception might be better and then you can verify the calls to the mocked classes.

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import java.net.InetAddress;
import java.net.UnknownHostException;

import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;


@RunWith(PowerMockRunner.class)
@PrepareForTest(InetAddress.class)
public class Mocker {

    @Mock
    private java.net.InetAddress mockedAddress;

    @Test
    public void testGetLocalHostName() throws Exception {

        //given
        PowerMockito.mockStatic(InetAddress.class);
        given(InetAddress.getLocalHost()).willReturn(mockedAddress);
        given(mockedAddress.getHostName()).willThrow(UnknownHostException.class);

        //when
        try {
            getLocalhostName();
        } catch (RuntimeException e) {
            PowerMockito.verifyStatic();
            InetAddress.getLocalHost();
            verify(mockedAddress).getHostName();
        }
    }

    public static String getLocalhostName() {
        try {
            return InetAddress.getLocalHost().getHostName();
        } catch (final UnknownHostException e) {
            throw new RuntimeException(e.getMessage());
        }
    }

}

I came across the same issue, and came up with this:

private static String getHostName() {
    // unit test trickery!!!
    // the TRY should simply return Inet4Address.getLocalHost().getHostName().
    // but, there's no way to force an UnknownHostException, so we can't
    // get 100% coverage. 100% coverage isn't entirely necessary, but it
    // was the only uncovered line in the entire project, so let's make it happen.
    // throw the exception manually and use it to propagate the host name to the
    // catch, then the catch returns it. BOOM! Sorcery.
    // This is only called once during the app life time, so we can take
    // a hit on the unnecessary exception.
    try {
        throw new UnknownHostException(Inet4Address.getLocalHost().getHostName());
    } catch (final UnknownHostException e) {
        return e.getMessage();
    }
}

It's used exactly once (the result is assigned to a variable, and the variable is reused), so I'm willing to to take the ethical hit.

Of course, if there really is an exception, the distinction is't made. The CATCH could be elaborated to figure that out. (IE: if any spaces in the result, then it's not a host name)

public static String getLocalhostName() throws UnknownHostException  {
      try {
         return InetAddress.getLocalHost().getHostName();
      }
      catch ( final UnknownHostException e ) {
         throw new RuntimeException( e.getMessage() );
      }
   }

@Test
public void testGetLocalhostName() throws UnknownHostException  {
  final String host = getLocalhostName();
  Assert.assertEquals( "mycomputer", host );
}

and use try..catch in main or declare it to throw Exception or UnKnownHostException

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM