PowerMock - Completing your unit testing toolkit
Let's face it: Mocking/Stubbing are so integral to proper unit testing, that you really can't unit test without it. But the standard libraries - EasyMock, Mockito, JMock, etc. - don't support mocking/stubbing the following uses cases out of the box:
- Static method calls (MyStaticClass.someStaticMethod)
- Constructors (new MyClass())
- Verify that a private method was called
Now, when ever you look this up you'll see a lot of responses along the lines of "if you need to do this, you're design is wrong...and you're a jerk!". Well, most time this is true, but the fact of the matter is you can have a really well design application BUT you are still using static methods and constructors. You can't use a factory in EVERY case, and besides: how would you properly unit test a factory unless you can verify what it constructs AND how it constructs it.
So one of the best things I ever found for unit testing in Java is PowerMock. By using it in conjunction with EasyMock or Mockito, you open up your mocking/stubbing toolset to include the aforementioned cases. Here's a unit test I wrote as part of a pet project I'm starting up, a web resource crawler and reporter called charlotte.
Take a look, learn it AND use it! May the unit tests be with you...
package com.hcisf.charlotte.loader;
import com.hcisf.PowerMockWithSpecTestNameRunner;
import com.hcisf.charlotte.domain.Resource;
import com.hcisf.charlotte.domain.ResourceStatus;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.powermock.api.mockito.PowerMockito;
import static org.mockito.Mockito.*;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(PowerMockWithSpecTestNameRunner.class)
@PrepareForTest({Jsoup.class, JsoupHttpLoader.class})
public class JsoupHttpLoaderTest {
private static final int TIMEOUT = 1000;
private static final String RESOURCE_URL = "http://google.com";
JsoupHttpLoader loader;
Resource resource;
Document document;
Elements elements;
@Before
public void setup() throws Exception {
PowerMockito.mockStatic(Jsoup.class);
resource = new Resource(RESOURCE_URL);
document = mock(Document.class);
elements = new Elements();
doReturn(elements).when(document).select(anyString());
when(
Jsoup.parse(any(URL.class), anyInt())
).thenReturn(
document
);
loader = new JsoupHttpLoader(TIMEOUT);
}
@Test
public void shouldAttemptToLoadTheContentAtTheProvidedResourceLocation() throws Exception {
// When the loader populates the resource
loader.populateResource(resource);
// Then parsing library is asked to parse the content at the resource location
PowerMockito.verifyStatic();
Jsoup.parse(any(URL.class), eq(TIMEOUT));
}
@Test
public void shouldReturnAResourceSetToParsedIfTheLoadedContentIsParsedSuccessfully() throws Exception {
// When the loader loads the resource URL
Resource output;
output = loader.loadResource(RESOURCE_URL);
// Then the returned resource is set to parsed
assertNotNull(output);
assertEquals(ResourceStatus.PARSED, output.status);
}
@Test
public void shouldReturnAResourceSetToInvalidIfLocationIsMalformed() throws Exception {
// Given the provided resource location is malformed
PowerMockito.whenNew(URL.class)
.withArguments(RESOURCE_URL)
.thenThrow(new MalformedURLException());
// When the loader loads the resource URL
Resource output;
output = loader.loadResource(RESOURCE_URL);
// Then the returned resource is set to invalid
assertNotNull(output);
assertEquals(ResourceStatus.INVALID, output.status);
}
@Test
public void shouldReturnAResourceSetToUnavailableIfCannotConnectToLocation() throws Exception {
// Given the provided resource location is unavailable
PowerMockito.whenNew(URL.class)
.withArguments(RESOURCE_URL)
.thenThrow(new IOException());
// When the loader loads the resource URL
Resource output;
output = loader.loadResource(RESOURCE_URL);
// Then the returned resource is set to unavailable
assertNotNull(output);
assertEquals(ResourceStatus.UNAVAILABLE, output.status);
}
@Test
public void shouldCreateAChildResourceForEveryLinkFoundInTheParsedDocument() throws Exception {
// Given the parsed document contains a link in it
Element element = mock(Element.class);
when(
element.attr(anyString())
).thenReturn(
RESOURCE_URL
);
elements.add(element);
// When the loader loads the resource URL
Resource output;
output = loader.loadResource(RESOURCE_URL);
// Then the returned resource has 1 child
assertNotNull(output.children);
assert output.children.size() == 1;
Resource child = output.children.get(0);
// Add the child is an empty resource with it's location set
assertEquals(ResourceStatus.EMPTY, child.status);
assertEquals(RESOURCE_URL, child.location);
}
}