unit testing & mocks - komputeralgebra...
TRANSCRIPT
Bad and
Best Practices
in Unit Testing & Mocking
Istvan Neuwirth 2014
Unit Test
• Tests for testing the smallest testable parts of the software (methods/classes)
Legend
Neutral/Others
Best Practice
Bad Practice
Unit Test Principles
• Fast
• Isolated
• Repeatable
• Self-verifying
• Timely
Test Doubles
• Dummy
• Stub
• Spy
• Mock
• Fake
Dummy
private static final Locale DUMMY = Locale.CHINA; @Test public void testGetYear() { Calendar calendar = new GregorianCalendar(DUMMY); calendar.set(Calendar.YEAR, 2013); assertEquals(2013, calendar.get(Calendar.YEAR)); }
Stub
public class PriceSummarizer { private final List<Product> products; private final CurrencyConverter converter; public PriceSummarizer(List<Product> products, CurrencyConverter converter) { this.products = products; this.converter = converter; } public BigDecimal calcPrices(Currency currency) { BigDecimal sum = BigDecimal.ZERO; for (Product product : products) { BigDecimal price = converter.convert(product.getCurrency() currency, product.getPrice()); sum.add(price); } return sum; } }
Stub
@Test public void testCalcPrices() { CurrencyConverter converter = Mockito.mock(CurrencyConverter.class); Mockito.when(converter.convert( Mockito.any(Currency.class), Mockito.any(Currency.class), Mockito.any(BigDecimal.class))) .thenReturn(BigDecimal.ONE); List<Product> products = createProducts(1); PriceSummarizer sut = new PriceSummarizer(products, converter); assertEquals(BigDecimal.ONE, sut.calcPrices(DUMMY)); }
Spy
public class GarbageCollector { private Finalizer finalizer; private Objects objects; public void gc() { stopTheWorld(); for (Object obj : objects) { if (thereIsNoReferenceFor(obj)) { finalizer.add(obj); } } compressHeap(); } ... }
Spy
@Test public void testGc_NonReferencedObject() { Finalizer finalizer = Mockito.mock(Finalizer.class); ArgumentCaptor<Object> captor = ArgumentCaptor.forClass(Object.class); GarbageCollector gc = createGC(); gc.setFinalizer(finalizer); gc.gc(); Mockito.verify(finalizer).add(captor.capture()); assertEquals(obj, captor.getValue()); }
Mockito Spy (Partial Mock)
List list = new LinkedList(); List spy = Mockito.spy(list); //optionally, you can stub out some methods: when(spy.size()).thenReturn(100); //using the spy calls *real* methods spy.add("one"); spy.add("two"); //prints "one" - the first element of a list System.out.println(spy.get(0)); //size() method was stubbed - 100 is printed System.out.println(spy.size()); //optionally, you can verify verify(spy).add("one"); verify(spy).add("two");
Source: http://docs.mockito.googlecode.com/hg/latest/org/mockito/Mockito.html#13
Mock
@Test public void testPut() { Map<Class<?>, Object> map = EasyMock.createMock(Map.class); ClassToInstanceMap sut = new ClassToInstanceMap(map); EasyMock.expect(map.put(Integer.class, 5)).andReturn(null); EasyMock.replay(map); sut.put(5); EasyMock.verify(map); }
Mock
@Test public void testPut() { Map<Class<?>, Object> map = Mockito.mock(Map.class); ClassToInstanceMap sut = new ClassToInstanceMap(map); sut.put(5); Mockito.verify(map).put(Integer.class, 5); }
Fake
• HSQLDB
• DeterministicScheduler (see it later)
static Formatter formatter; @BeforeClass public static void setUp() { formatter = new Formatter(); } @Test public void testOnWindows() { Printer printer = new Printer(formatter); Assert.assertEquals("aa\r\nbb", printer.printToString("aa", "bb")); formatter.setEndline("\n"); // <- WTF? } @Test public void testOnUnix() { Printer printer = new Printer(formatter); Assert.assertEquals("aa\nbb", printer.printToString("aa", "bb")); }
Avoid: order-dependent tests
Formatter formatter; @Before public void setUp() { formatter = new Formatter(); } @Test public void testOnWindows() { formatter.setEndline("\r\n"); Printer printer = new Printer(formatter); Assert.assertEquals("aa\r\nbb", printer.printToString("aa", "bb")); } @Test public void testOnUnix() { formatter.setEndline("\n"); Printer printer = new Printer(formatter); Assert.assertEquals("aa\nbb", printer.printToString("aa", "bb")); }
…better:
@Test public void testOnWindows() { Printer printer = newPrinterWithEndline("\r\n"); Assert.assertEquals("aa\r\nbb", printer.printToString("aa", "bb")); } @Test public void testOnUnix() { Printer printer = newPrinterWithEndline("\n"); Assert.assertEquals("aa\nbb", printer.printToString("aa", "bb")); } private Printer newPrinterWithEndline(String endline) { Formatter formatter = new Formatter(); formatter.setEndline(endline); return new Printer(formatter); }
…or:
@Test public void testPay_ShouldReduceMoney() { Customer customer = new Customer(10); // 10 fabatka customer.pay(4); Assert.assertEquals(6, customer.getMoneyAmount()); Assert.assertEquals(Mood.HAPPY, customer.getMood()); } @Test public void testPay_MakeBadMood() { Customer customer = new Customer(10); // 10 fabatka customer.pay(8); Assert.assertEquals(2, customer.getMoneyAmount()); Assert.assertEquals(Mood.SAD, customer.getMood()); }
Avoid: multiple logical assertions
@Test public void testPay_ShouldReduceMoney() { Customer customer = new Customer(10); // 10 fabatka customer.pay(4); Assert.assertEquals(6, customer.getMoneyAmount()); } @Test public void testPay_ALittle_ShouldCauseHappiness() { Customer customer = new Customer(10); // 10 fabatka customer.pay(2); Assert.assertEquals(Mood.HAPPY, customer.getMood()); } @Test public void testPay_ALot_ShouldCauseSadness() { Customer customer = new Customer(10); // 10 fabatka customer.pay(8); Assert.assertEquals(Mood.SAD, customer.getMood()); }
…instead:
private final Splitter splitter = Splitter.on(','); @Test public void testSplit1() { assertEquals(list("a", "b", "c"), splitter.omitEmptyStrings().split("a,,b,c")); } @Test public void testSplit2() { assertEquals(list(), splitter.omitEmptyStrings().split(",,")); } @Test public void testSplit3() { assertEquals(list("a", "x", ""), splitter.trimResults().split(" a, x ,")); }
Do NOT use numbered names
private final Splitter splitter = Splitter.on(','); @Test public void testSplit_OmitEmptyStrings() { assertEquals(list("a", "b", "c"), splitter.omitEmptyStrings().split("a,,b,c")); assertEquals(list(), splitter.omitEmptyStrings().split(",,")); } @Test public void testSplit_TrimResults() { assertEquals(list("a", "x", ""), splitter.trimResults().split(" a, x ,")); }
…rename them:
public class CustomerTest { @Test public void testCustomer() { Customer customer = new Customer(10); // 10 fabatka customer.pay(4); Assert.assertEquals(6, customer.getMoneyAmount()); } }
Do NOT give misleading names I.
public class CustomerTest { @Test public void testCustomerWonTheLottery() { Customer customer = new Customer(10); // 10 fabatka customer.pay(4); Assert.assertEquals(6, customer.getMoneyAmount()); } }
Do NOT give misleading names II.
public class CustomerTest { @Test public void testPay_ShouldReduceMoney() { Customer customer = new Customer(10); // 10 fabatka customer.pay(4); Assert.assertEquals(6, customer.getMoneyAmount()); } }
…rename them:
public class CustomerTest { @Test public void testCustomer_WithOneParameter() { Customer customer = new Customer(10); // 10 fabatka Assert.assertEquals(10, customer.getMoneyAmount()); } }
Do NOT give misleading names III.
public class CustomerTest { @Test public void testConstructor_WithMoney() { Customer customer = new Customer(10); // 10 fabatka Assert.assertEquals(10, customer.getMoneyAmount()); } }
…rename them:
Avoid: too long or too short names
0
50
100
150
200
250
300
350
5 15 25 35 45 55 65 75 85 95 105 115 125
Length of Java test method names (without 'test' prefix)
Frequency
Results collected from some of our projects
@Test(expected = FileNotFoundException.class) public void testReadFileShouldThrowFileNotFoundExceptionWhenFileIsNotFound() { }
Avoid: too long names
@Test(expected = FileNotFoundException.class) public void testReadFile_ShouldThrowFileNotFoundException_WhenFileIsNotFound() { }
…consider: separator in names
@Test(expected = FileNotFoundException.class) public void testReadFile_FileDoesNotExist() { }
…and omit redundant phases:
Assert.assertEquals(null, sut.getValue()); Assert.assertEquals(true, sut.isFinished()); Assert.assertEquals(false, sut.isInProgress());
Avoid: using assertEquals only
Assert.assertNull(sut.getValue()); Assert.assertTrue(sut.isFinished()); Assert.assertFalse(sut.isInProgress());
…instead:
Assert.assertTrue(sut.getValue() == null); Assert.assertTrue(sut.isFinished() == true); Assert.assertTrue(sut.isInProgress() == false);
Avoid: using assertTrue only
Assert.assertNull(sut.getValue()); Assert.assertTrue(sut.isFinished()); Assert.assertFalse(sut.isInProgress());
…instead:
public class CustomerTest { }
Don’t create empty test classes
public class CustomerTest { @Ignore("test the passed values for different types") @Test public void testConstructor_WithMoney() { // ... } }
…add some searchable helper:
// FIXME write tests public class SplitterTest { // TODO write tests for splitter with ommitting empty strings // TODO write tests for splitter with trimming splitted values // TODO write tests for boundary cases (empty, non-null string) // TODO write negative tests for null reference }
…add some searchable helper:
Avoid: to test private, final or static stuffs; to mock constructor
…or you can use PowerMock
Time-dependent Tests
• System.currentTimeMillis()
• PowerMockito?
– Nope!
Time-dependent Code
public class Stopwatch { private long start; public Stopwatch() { start(); } public long start() { return start = System.currentTimeMillis(); } public long elapsedMillis() { return System.currentTimeMillis() - start; } }
@Test public void testElapsedMillis() throws InterruptedException { Stopwatch sut = new Stopwatch(); sut.start(); Thread.sleep(1000); assertEquals(1000, sut.elapsedMillis(), 100); }
Don’t do this! Never ever.
import org.joda.time.DateTimeUtils; public class Stopwatch { private long start; public Stopwatch() { start(); } public long start() { return start = DateTimeUtils.currentTimeMillis(); } public long elapsedMillis() { return DateTimeUtils.currentTimeMillis() - start; } }
…instead:
@Test public void testElapsedMillis() throws InterruptedException { MillisProvider millisProvider = Mockito.mock(MillisProvider.class); Mockito.when(millisProvider.getMillis()).thenReturn(0L, 50L, 1050L); DateTimeUtils.setCurrentMillisProvider(millisProvider); Stopwatch sut = new Stopwatch(); sut.start(); assertEquals(1000, sut.elapsedMillis()); }
…and:
Testing Concurrent Code
• Fast?
• Repeatable?
Concurrent Code
public class SimpleScheduler { private ScheduledExecutorService executor = new ScheduledThreadPoolExecutor( Runtime.getRuntime().availableProcessors()); private final Runnable runnable; private final int tickIntervall; public SimpleScheduler(int tickIntervall, Runnable runnable) { this.runnable = runnable; this.tickIntervall = tickIntervall; } public void start() { executor.scheduleAtFixedRate(runnable, 0, tickIntervall, TimeUnit.MILLISECONDS); } }
private ConcurrentLinkedQueue<Long> timeStamps = new ConcurrentLinkedQueue<Long>(); @Test public void testSchedulingTasks_() throws Exception { Runnable runnable = createRunnable(); SimpleScheduler scheduler = new SimpleScheduler(1000, runnable); scheduler.start(); Thread.sleep(2500); assertEquals(3, timeStamps.size()); } private Runnable createRunnable() { return new Runnable() { @Override public void run() { timeStamps.add(System.currentTimeMillis()); } }; }
Don’t do this
@Test public void testSchedulingTasks() { Runnable runnable = createRunnable(); SimpleScheduler scheduler = new SimpleScheduler(1000, runnable); final DeterministicScheduler executor = new DeterministicScheduler(); scheduler.setExecutor(executor); scheduler.start(); executor.tick(2500, TimeUnit.MILLISECONDS); assertEquals(3, timeStamps.size()); }
…instead:
Concurrent Code Tests
• jMock library
– DeterministicScheduler
– DeterministicExecutor
http://jmock.org/