Effective Unit Testing with Test Doubles (GDG Boulder, June 2016)

Effective Unit Testing with Test Doubles (GDG Boulder, June 2016)

A test double is any object that takes the place of a real object in a test, just like a stunt double takes the place of an actor in a movie. There are several types of test doubles, including stubs, fakes, dummies, mocks, and spies. In this talk, we'll explore the common uses of these types of test doubles, their advantages and pitfalls, and best practices for using test doubles to write effective and reliable unit tests.

http://www.meetup.com/Google-Developer-Group-Boulder/events/231374141/

https://www.youtube.com/watch?v=_pCwcdNtxog

8d9b8aa31d299a7bc2211f4a4a517215?s=128

Matt Logan

June 30, 2016
Tweet

Transcript

  1. Matt Logan Effective Unit Testing with Test Doubles

  2. • Terminology • Test philosophy • Types of test doubles

    • Best practices • Full example • Extra considerations Outline
  3. • System under test • Depended-on component • Indirect output

    • Indirect input • Test double Terminology
  4. –Martin Fowler “Test Double is a generic term for any

    case where you replace a production object for testing purposes.”
  5. Why? • Enable testing • Ease testing • Improve testing

    • NOT just to “isolate” system under test
  6. Test philosophy • Tests should: • Verify correct behavior (or

    state) • Allow for safe refactoring • Drive good design • Serve as documentation • Be easy to write!
  7. • Dummy • Fake • Stub • Mock • Spy

    Types of test doubles
  8. Dummies • Never used • Satisfy method parameters • Easy

    to implement • Primitives or objects
  9. Dummies class DummyClient implements LocationClient {
 
 @Override public void

    startUpdates(Callback c) {
 throw new RuntimeException("Dummy!");
 }
 }
  10. Dummies • More examples • -1 • “String that will

    not be used” • null
  11. Fakes • Replace a “depended-on component” with a lighter weight

    implementation • Takes shortcuts, but still functional • Can be used for input or output • Usually hand-coded • Example: in-memory database • Another example: fake web service
  12. Fakes class FakeDb implements LocationDatabase {
 Map<String, Location> map =

    new HashMap<>();
 
 @Override
 public void save(String key, Location loc) {
 map.put(key, loc);
 }
 
 @Override
 public Location get(String key) {
 return map.get(key);
 }
 }
  13. Stubs • Control indirect inputs to system under test •

    Provide “canned responses” • Example: stubbed HTTP responses
  14. Stubs class LocationEnabledDevice
 implements DeviceInfo {
 
 @Override
 public boolean

    isLocationEnabled() {
 return true;
 }
 }
  15. Stubs // Create the “mock” DeviceInfo deviceInfo = mock(DeviceInfo.class); //

    “Stub” its method(s)
 when(deviceInfo.isLocationEnabled()) .thenReturn(false);
  16. Mocks • Mocks “expect” • Good for verifying exact behavior

    • Good for strict TDD • Can lead to over-specification • Also a generic (confusing) term for any test double • Also a verb for creating a test double
  17. Mocks // Create system under test with mock object expect(callbacks

    .onLocationUnavailable()); // Exercise system under test // Will throw if method not called
  18. Spies • Very similar to mocks • Record interactions, “verify”

    later • Better at showing intent — can hide irrelevant calls • Debugging can be harder
  19. Spies // Create system under test with spy // Exercise

    system under test verify(callbacks) .onLocationUnavailable(); // Will throw if method not called
  20. Best practices • Don’t use doubles for values! • Examples:

    Location, Date, Employee • Just create them • Don’t need to verify that accessors are called • If object creation is complicated, try builders
  21. Best practices • Don’t use doubles for classes — just

    interfaces! • Interfaces show relationships between objects • Extract “what you need” into an interface • Need interfaces to create hand-coded doubles
  22. Best practices • Don’t test implementation details! • Ask question:

    “Is this is a required behavior?” • Leads to brittle, unreliable tests • Refactoring implementation without changing API should NEVER break tests.
  23. Location Tracker github.com/mattlogan/locationtracker

  24. public void trackLocation() {
 if (!device.isLocationEnabled()) {
 callbacks.onLocationUnavailable();
 return;
 }


    
 client.startUpdates(new Callback() {
 @Override
 public void onUpdate(Location curLoc) {
 if (!curLoc.equals(lastLoc)) {
 callbacks.onChange(curLoc);
 }
 lastLoc = curLoc;
 }
 });
 }
  25. class DummyClient implements LocationClient {
 
 @Override public void startUpdates(Callback

    c) {
 throw new RuntimeException("Dummy!");
 }
 }
  26. class NoLocationDevice
 implements DeviceInfo {
 
 @Override
 public boolean isLocationEnabled()

    {
 return false;
 }
 }
  27. @Test
 public void locationUnavailableCalled() {
 DeviceInfo device = // Stub

    new NoLocationDevice();
 LocationClient client = // Dummy new DummyClient();
 Callbacks callbacks = // Spy mock(Callbacks.class);
 LocationChangeTracker tracker =
 new LocationChangeTracker(device,
 client,
 callbacks);
 tracker.trackLocation();
 verify(callbacks) .onLocationUnavailable();
 }
  28. public void trackLocation() {
 if (!device.isLocationEnabled()) {
 callbacks.onLocationUnavailable();
 return;
 }


    
 client.startUpdates(new Callback() {
 @Override
 public void onUpdate(Location curLoc) {
 if (!curLoc.equals(lastLoc)) {
 callbacks.onChange(curLoc);
 }
 lastLoc = curLoc;
 }
 });
 }
  29. class FakeClient implements LocationClient {
 List<Location> locs = new ArrayList<>();


    
 FakeClient(Location... locs) {
 Collections.addAll(this.locs, locs);
 }
 
 @Override
 public void startUpdates(Callback c) {
 for (Location loc : locs) {
 c.onUpdate(loc);
 }
 }
 
 @Override
 public void stopUpdates() {}
 }
  30. class LocationEnabledDevice
 implements DeviceInfo {
 
 @Override
 public boolean isLocationEnabled()

    {
 return true;
 }
 }
  31. @Test
 public void onChangeCalled() {
 DeviceInfo device = // Stub


    new LocationEnabledDevice();
 LocationClient client = // Fake
 new FakeClient(new Location(1, 0),
 new Location(2, 2),
 new Location(2, 2));
 Callbacks callbacks = // Spy
 mock(Callbacks.class);
 LocationChangeTracker tracker =
 new LocationChangeTracker(device,
 client,
 callbacks);
 tracker.trackLocation();
 verify(callbacks, times(2))
 .onChange(isA(Location.class));
 }
  32. Extra considerations • Testing behavior vs. testing state • Test

    doubles aren’t just for “unit” tests • Dependency injection helps • Fakes can become production objects • It’s okay to write tests for your test doubles
  33. Contact + resources • Contact & follow me (if you

    want to) • matt.b.logan@gmail.com • @_mattlogan • mattlogan.me • More resources • github.com/mattlogan/locationtracker • Mocks Aren’t Stubs - Martin Fowler • Mocks Aren’t Stubs, Dummies, Fakes or Spies - Dave Marshall • xUnit Test Patterns - Gerard Meszaros