Software Engineering at Google Book Notes
July 1, 2022•767 words
Chapter 12: Unit Testing
#studywithme
Notes
A unit test is maintainable when
- they "just work" after writing them
- you don't have to think about them until they fail and those failures indicate real bugs
- the tests will outlive you.
Unit tests shouldn't change when
- there's a refactor
- new features are added
- there's a bug fix (it's like adding a new feature)
Unit tests should change when
- behavior changes
To prevent brittle tests you should
test via public APIs
- testing private APIs results in many brittle tests
How do you know if it's a public API?
- helper method or helper classes? are public APIs
- packages or classes accessible to anyone? are public APIs
test state, not interactions
- usually tests test interactions
- Tests that test interactions won't work because the interaction can cause a side-effect and fail the test
- Instead, test the state after the interaction happens
Example (from the book)
Test that tests interaction
@Test public void shouldWriteToDatabase() { accounts.createUser("foobar"); verify(database).put("foobar"); }
Test that tests state
@Test public void shouldCreateUsers() { accounts.createUser("foobar"); assertThat(accounts.getUser("foobar")).isNotNull(); }
To write clearer tests
Make your tests complete and concise
A test can fail for two reasons
- 1. the system is incomplete
- 2. the test itself is flawed
Tests should be complete
- All of the relevant context and information should be in the test itself
Tests should be concise
- There shouldn't be any irrelevant or distracting information
How to
- Don't pass a lot of irrelevant information into the constructor
- Don't heavily rely on helper methods
A bad example
@Test public void shouldPerformAddition() { Calculator calculator = new Calculator(new RoundingStrategy(), "unused", ENABLE_COSINE_FEATURE, 0.01, calculusEngine, false); int result = calculator.calculate(newTestCalculation()); assertThat(result).isEqualTo(5); }
A good example
@Test public void shouldPerformAddition() { Calculation calculator = newCalculation(); int result = calculator.calculate(newCalculation(2, Operation.PLUS, 3)); }
Test behaviors, not methods
- Behaviors can be expressed by
Given
,When
,Then
Good example
@Test public void transferFundsShouldMoveMoneyBetweenAccounts() { // Given two accounts with initial balances of $150 and $20 Account account1 = newAccountWithBalance(usd(150)); Account account2 = newAccountWithBalance(usd(20)); // When transferring $100 from the first to the second account bank.transferFunds(account1, account2, usd(100)); // Then the new account balances should reflect the transfer assertThat(account1.getBalance()).isEqualTo(usd(50)); assertThat(account2.getBalance()).isEqualTo(usd(120)); }
You can alternate when/then blocks, but for single behavior
collapsed:: true@Test public void shouldTimeOutConnections() { // Given two users User user1 = newUser(); User user2 = newUser(); // And an empty connection pool with a 10-minute timeout Pool pool = newPool(Duration.minutes(10)); // When connecting both users to the pool pool.connect(user1); pool.connect(user2); // Then the pool should have two connections assertThat(pool.getConnections()).hasSize(2); // When waiting for 20 minutes clock.advance(Duration.minutes(20)); // Then the pool should have no connections assertThat(pool.getConnections()).isEmpty(); // And each user should be disconnected assertThat(user1.isConnected()).isFalse(); assertThat(user2.isConnected()).isFalse(); }
If you're using the word "and" in a test there's a good chance you should have two tests
- Behaviors can be expressed by
Don't put logic in tests
- It's very easy to make a mistake when there's logic in the test
- Don't even have string concatenation, just repeat the value that you had in the variable
Write clear failure messages
Failure messages should have
- What was given
- What was expected
- Information about the state
Example
-
Expected an account in state CLOSED, but got account: <{name: "my-account", state: "OPEN"}
-
DAMP > DRY
DAMP
- Descriptive and Meaningful Phrases
DRY
- Don't Repeat Yourself
a little bit of duplication is OK in tests so long as that duplication makes the test simpler and clearer
Shared values
- As a whole, try to avoid them
- They can make the code concise, but you lose context
- You can use a helper function to replace them
- Example
Shared Setup
- It's better to have a set up and then override is than just have a setup
- Example
TL;DR
- Strive for unchanging tests
- Test via public APIs
- Test state, not interactions
- Make your tests complete and concise
- Test behaviors, not methods
- Structure tests to emphasize behaviors
- Name tests after the behavior being tested
- Don't put logic in tests
- Write clear failure messages
- Follow DAMP over DRY when sharing code for tests