CantChangeMe / Unit-Testing-Cheat-Sheet

Based on "Working Effectively with Unit Tests" by Jay Fields

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

WORKING EFFECTIVELY WITH UNIT TESTS

Types of tests

State Verification

Assert the expected state of the object and/or collaborators.

  public class RentalTest {
    @Test
    public void rentalIsStartedIfInStore() {
      Movie movie = a.movie.build();
      Rental rental = a.rental.w(movie).build();
      Store store = a.store.w(movie).build();
      rental.start(store);
      assertTrue(rental.isStarted());
      assertEquals(0, store.getAvailability(movie));
    }
  }

Rental is the Subject Under Test (SUT) or Class Under Test (CUT) and Store a collaborator

State verification tests generally rely on assertions to verify the state of our objects.

Behavior Verification

The test expect to generate specific interactions between objects.

public class RentalTest {
 @Test
  public void rentalIsStartedIfInStore() {
    Movie movie = a.movie.build();
    Rental rental = a.rental.w(movie).build();
    Store store = mock(Store.class);
    when(store.getAvailability(movie)).thenReturn(1);
    rental.start(store);
    assertTrue(rental.isStarted());
    verify(store).remove(movie);
  }
}

Unit Tests

Solitary Unit Test

Unit test at the class level, but:

  1. Never cross boundaries
  2. The Class Under Test should be the only concrete class found in a test.
public class MovieTest {
  @Test
  (expected=IllegalArgumentException.class)
  public void invalidTitle() {
    a.movie.w(UNKNOWN).build();
  }
}

Sociable Unit Test

Any Unit Test that cannot be classified as a Solitary Unit Test is a Sociable Unit Test.

These tests have 2 potential issues:

  • They run the risk of failing due to an implementation change in a collaborator.
  • When they fail it can be hard to determine if the issue is coming from the Class Under Test, a collaborator, or somewhere else completely.

Mitigate the above issues with the following suggestions.

  • Verify as much as you can with 1 happy path test per method. When things do go wrong, you want as little noise as possible. Limiting the number of Sociable Unit Tests can go a long way to helping the situation when things go wrong.
  • If you stick to fixing the Solitary Unit Tests before the Sociable Unit Tests, by the time you get to a failing Sociable Unit test you should have a very good idea where to find the root of the problem.

Improving Assertions

One Assertion Per Test.

Test Naming should express the intent of the test.

  • If your test has an assertion, do not add any mock verifications.
  • If your test verifies a mock, do not add any assertions.
  • At most, 1 assertion per test.
  • At most, 1 mock verification per test.
  • When stubbing method return values, use the most generic argument matcher possible.

Implementation Overspecification

The more specification your tests contain the more likely you are to create a fragil test suite.

Flexible Argument Matchers

Use Mockito’s anyInt, anyString and anyBoolean

movie.getTitle(anyString(), anyInt()))

Default Return Values

Return values to avoid a NullPointerException however we don't always need a return

Law of Demeter

Only talk to your immediate friends. Reduce or eliminate the relations between the objects.

public class CustomerTest {
  @Test
  public void recentRentals2Rental() {
    assertEquals(
      "Recent rentals:\nnull\nnull",
      a.customer.w(
        mock(Rental.class),
        mock(Rental.class)).build()
        .recentRentals()
      );
  }
...
}  

Get Sociable

After having all the unitary tests we can be sociable tests based on them.

public class CustomerTest {
  ...
  @Test
  public void recentRentalsWith3OrderedRentals() {
    assertEquals(
      "Recent rentals:"+
      "\nGodfather 4\nLion King\nScarface",
      a.customer.w(
        a.rental.w(a.movie.w("Godfather 4")),
        a.rental.w(a.movie.w("Lion King")),
        a.rental.w(a.movie.w("Scarface")),
        a.rental.w(a.movie.w("Notebook")))
        .build().recentRentals()
      );
    }
}

Assert Last

The assertion should be the last piece of code found within a test.

“Arrange-Act-Assert”

  1. Arrange all necessary preconditions and inputs.
  2. Act on the object or method under test.
  3. Assert that the expected results have occurred.

Assert Last is where we should focus if we can not get the 3 As.

Expect Exceptions via Try/Catch

public class MovieTest {
  @Test
  public void invalidTitle() {
    Exception e = null;
    try {
      a.movie.w(UNKNOWN).build();
    } catch (Exception ex) {
      e = ex;
    }
    assertEquals(
      IllegalArgumentException.class,
      e.getClass()
    );
  }
}

This is not perfect. Better here 👇

Assert Throws

public class MovieTest {
  
  @Test
  public void invalidTitle() {
    Runnable runnable = new Runnable() {
      public void run() {
        a.movie.w(UNKNOWN).build();
      }
    };
    assertThrows( IllegalArgumentException.class, runnable);
  }

  public void assertThrows(
    Class ex, Runnable runnable) {
    Exception exThrown = null;
    try {
      runnable.run();
    } catch (Exception exThrownActual) {
      exThrown = exThrownActual;
    }
    if (null == exThrown)
      fail("No exception thrown");
    else
      assertEquals(ex, exThrown.getClass());
  }
}

This could be simplified using JAVA 8 lambdas or kotlin

Expect Literals

The expected value should be the literal itself, not a variable. In case of money, dates, or similars try to convert them to literals

Negative Testing

Are tests that assert something did not happen. Don't do it.

Just Be Sociable

Ask yourself :

Why is it hard to get positive ROI out of this test?

Why am I testing this interaction in the first place?

You will probably end with a different test or tests.

Improving Test Cases

An effective test suite implies maintainable test cases.

Tests are procedural by nature.

  • The primary motivation for naming a test method is documentation
  • Instance method collaboration is considered an antipattern.
  • Each instance method is magically given its own set of instance variables
  • Each test method should encapsulate the entire lifecycle and verification, independent of other test methods.
  • Tests methods should have very specific goals which can be easily identified by maintainers.

Too Much Magic

Remove complex code when implementing tests. They should be simple and straightforward to understand.

Inline Setup

If you aspire to create tiny universes with minimal conceptual overhead, rarely will you find the opportunity to use Setup( @Before ).

Similar Creation and Action

Reduce creation duplication by introducing globally used builders.

Duplicate code is a smell. Setup and Teardown are deodorant.

Setup As An Optimization

Creating a database connection per Sociable Unit Test would be slower than creating one in each test within a single Test Case.

Why are we creating more than one database connection at all?

Why not create one global connection and run each test in a transaction that’s automatically rolled back after each Sociable Unit Test?

Answer yourself these questions

  • Are all the Sociable Unit Tests still necessary?
  • Are the interactions with the File System, Database, and/or Messaging System still necessary?
  • Is there a faster way to accomplish any of the tasks setting the File System, Database and/or Messaging System back to a known state?

Test Names

Test names are like comments.

Code never lies, comments sometimes do

Improving Test Suites

Separating The Solitary From The Sociable

  1. Sociable Unit Tests can be slow and nondeterministic
  2. Sociable Unit Tests are more susceptible to cascading failures

Increasing Consistency And Speed With Solitary Unit Tests

What affects the more in tests speed:

  • Interacting with a database
  • Interacting with the filesystem
  • Interacting with time

Database and Filesystem Interaction

Wrapping the commonly used libraries with a gateway that provides the following capabilities:

  • The ability to disallow access within Solitary Unit Tests
  • The ability to reset to a base state before each Sociable Unit Test.
public class FileWriterGateway extends FileWriter {
  public static boolean disallowAccess = false;
  public FileWriterGateway(String filename) throws IOException {
    super(filename);
    if (disallowAccess) throw new RuntimeException("access disallowed");
  }
}
public class Solitary {
  @Before
  public void setup() {
    FileWriterGateway.disallowAccess = true;
  }
}
public class PidWriterTest extends Solitary {
@Test
  public void writePid() throws Exception {
    RuntimeMXBean bean = mock(RuntimeMXBean.class);
    when(bean.getName()).thenReturn("12@X");
    FileWriterGateway facade = mock(FileWriterGateway.class);
    PidWriter.writePid(facade, bean);
    verify(facade).write("12");
  }
}

Using Speed To Your Advantage

Convert a Sociable Unit Tests to a Solitary Unit Tests provides approximately the same ROI.

Faster feedback of equal quality.

Always run all of the Solitary Unit Tests first, and run the Sociable Unit Tests if and only if all of the Solitary Unit Tests pass.

Avoiding Cascading Failures With Solitary Unit Tests

  1. Sociable Unit Tests are more susceptible to cascading failures
  2. Few things kill productivity and motivation faster than cascading test failures.

Questionable Tests

  • Don't Test Language Features or Standard Library Classes
  • Don't Test Framework Features or Classes
  • Don't Test Private Methods

Custom Assertions

T ake assert structural duplication and replace it with a concise, globally useful single assertion.

Structural Duplication: The overall pattern of the code is the same, but the details differ.

@Test
public void invalidTitleCustomAssertion() {
  assertThrows(
    IllegalArgumentException.class,
    () -> a.movie.w(UNKNOWN).build());
  }

Custom Assertions on Value Objects

Make them if there are several assertions that focused on a non Literal type like date.

public static void assertDateWithFormat(String expected, 
                                        String format, 
                                        Date date ){
  assertEquals(expected, new SimpleDateFormat(format).format(date));
}

Global Definition

If you find yourself repeating the same idea in multiple test cases, look for a higher level concept that can be extracted and reused.

Object Mother

An object with a fixture of the data used for a test.

CONS: As the project grows the coupling between the tests and the objects grows bigger and errors start to appear.

Test Data Builders

For each class you want to use in a test, create a Builder for that class that:

  • Has an instance variable for each constructor parameter
  • Initializes its instance variables to commonly used or safe values
  • Has a build method that creates a new object using the values in its instance variables
  • Has “chainable” public methods for overriding the values in its instance variables.

PROS: Data Builders provide you the benefits of creating an object with sensible defaults, and provide methods for adding your test specific data - thus keeping your tests decoupled.

Test Data Builder Syntax

Choose one an use the same always.

  1. anOrder().from(aCustomer().with(...)).build();
  2. a.order.w(a.customer.w(...)).build();
  3. build(order.w(customer.w(...)));

Creating Stubs

Create, Stub, Return

  1. Create a stub.
  2. Stub the result of a single method call.
  3. Pass the stub to another method.

About

Based on "Working Effectively with Unit Tests" by Jay Fields