EXPEDIA GROUP TECHNOLOGY — ENGINEERING

Healthy unit tests for healthy code bases

Five people sitting on a wall looking at the Colloseum, Rome
Photo by Jeff Marini

Reset the Mocks

Many unit tests require using some mocks to simulate external states and/or behaviour. Initializing those mocks often involves some common steps, and then configuring the state/behaviour needed for every single test.

const mockExternalStore = {
isValid: true
};

describe('UnderTest', () => {
it('is valid if external store is', () => {
const underTest = new UnderTest(mockExternalStore);
expect(underTest.isValid()).to.be.equal(true);
});

it('is not valid if external store is not', () => {
mockExternalStore.isValid = false;
const underTest = new UnderTest(mockExternalStore);
expect(underTest.isValid()).to.be.equal(false);
});
}
let mockExternalStore;

describe('UnderTest', () => {
beforeEach(() => {
mockExternalStore = {
isValid: true
};
});

it('is valid if external store is', () => {
// I assume mockExternalStore.isValid == true is the agreed starting scenario, so don't assign it again
const underTest = new UnderTest(mockExternalStore);
expect(underTest.isValid()).to.be.equal(true);
});

it('is not valid if external store is not', () => {
mockExternalStore.isValid = false;
const underTest = new UnderTest(mockExternalStore);
expect(underTest.isValid()).to.be.equal(false);
});
}

Be Careful with Exceptions

Sometimes we want to test that some illegal scenario leads to a specific exception being thrown. This can be done easily but is a good example of something that requires a little bit of care. Consider this example:

@Test
public void whenCallingForbiddenMethodThenThrowCustomException() {
try {
underTest.forbiddenMethod();
} catch (Exception e) {
assertTrue(e instanceof MyCustomException);
}
}
@Test(expectedException = MyCustomException.class)
public void whenCallingForbiddenMethodThenThrowCustomException() {
underTest.forbiddenMethod();
}
@Test
public void whenCallingForbiddenMethodThenThrowCustomException() {
try {
underTest.forbiddenMethod();
fail();
} catch (MyCustomException e) {
assertTrue(e.getCause() instanceof AnotherCustomException);
}
}

Prefer Explicit Test Data

Several tests can be replicated with different input parameters, in order to cover a wider array of regular and edge cases. Whether you do it manually or using some fancy @DataProvider, you must provide the set of input parameters.

public final String MY_KEY = "4e9aa2e2-0c29-4d01-a4fe-b464fd89ef74";

// Hashes input together with MY_KEY
public String hash(String input) {
...
}
@Test(dataProvider = "hashSamples")
public void hashingTest(String input, String expectedDigest) {
assertEquals(underTest.hash(input), expectedDigest);
}

@DataProvider
private Object[][] hashSamples() {
return new Object[][]{
{"input1", "E6d212KuLc0XvXsc"},
{"loooooonginpuuuuuuuuuut", "SCNb9HHscUPzCNHL"},
{"input+with-symbol$", "5ePJWwMwYwQBxLf9"},
{"", "aixwFwUnRmU1405D"},
{null, "4aHg05q4ftFzn7dX"}};
}
@Test(dataProvider = "hashSamples")
public void hashingTest(String input, String expectedDigest) {
assertEquals(underTest.hash(input), expectedDigest);
}

@DataProvider
private Object[][] hashSamples() {
return new Object[][]{
buildSample("input1", MY_KEY},
buildSample("loooooonginpuuuuuuuuuut", MY_KEY},
buildSample("input+with-symbol$", MY_KEY},
buildSample("", MY_KEY},
buildSample(null, MY_KEY}};
}

public Object[] buildSample(String input, String key) {
String digest = ... //Does this code look familiar?!?
return new Object[]{input, digest};
}

Now you are basically replicating production code in the test, which defeats the purpose of the test itself.

This was a rather extreme example, but this can happen at a smaller scale unless you watch out for this.

Do Not Change Production Code for the Sake of Tests

In common practice, the one who develops the code also develops the tests. That leads to the wrong assumption that the two should be shaped together.

public class BlackBox {
public doHouseCleaning() {
sweepTheFloor()
// ... some more code
doTheLaundry()
// ... some more code
doTheDishes()
// ... some more code
}

private sweepTheFloor() {
// ... a long method
}

private doTheLaundry() {
// ... a very long method
}

private doTheDishes() {
// ... an extremely long method
}
}
public class HouseCleaner {
public doHouseCleaning() {
broom.sweepTheFloor()
// ... some more code
washingMachine.doTheLaundry()
// ... some more code
dishWasher.doTheDishes()
// ... some more code
}
}

public class Broom {
public sweepTheFloor() {
// ...
}
}

public class WashingMachine {
public doTheLaundry() {
// ...
}
}

public class DishWasher {
public doTheDishes() {
// ...
}
}

Learn more about technology at Expedia Group

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store