Software Engineering at Google Book Notes

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

    • 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