CodeRetreat

Today is about learning & practicing

Today is about learning & practicing

What is automated testing?

  • A brief code snippet to verify that other codes behave as expected

  • Automatic, repeatable

  • Fast & deterministic

  • Code's test is the test, the test's test is the code

Test-Driven Development

A goal is Clean Code!

  1. Don’t write a line of new code unless you first have a failing automated test

  2. Eliminate duplication

Two rules of TDD

by Kent Beck

  1. Write a test…

  2. Make it run…

  3. Make it right…

A goal is loose coupling!

  1. You are not allowed to write any production code unless it is to make a failing unit test pass.

  2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.

  3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

Three rules of TDD

by Uncle Bob Martin

A goal is documenting!

  1. Write a test for the next bit of functionality you want to add

  2. Write the functional code until the test passes

  3. Refactor both new and old code to make it well structured

Three rules of TDD

by Martin Fowler

A goal is easy refactoring!

About TDD

by James Shore

A goal is fast cycles via small steps!

Test Driven Development

by Draven

A goal is planning and

experimenting in the code

A goal is to build trust
in the codebase

GIMME

EXAMPLE

String Calculator Kata

int add(String numbers)

add("")         -> 0
add("4")        -> 4
add("1,2")      -> 3
add("1,2,3")    -> 6
add("1\n2,3")   -> 6   (newline is also a separator)
add("//;\n1;2") -> 3   (custom delimiter)

The task

String Calculator Kata

public class StringCalculatorTest {
    @Test
    void shouldReturnZeroForEmptyString() {
        StringCalculator calculator = new StringCalculator();
        assertEquals(0, calculator.add(""));
    }
}

Compilation Error

(The class and method don't exist yet)

RED STATE

String Calculator Kata

public class StringCalculatorTest {
    @Test
    void shouldReturnZeroForEmptyString() {
        StringCalculator calculator = new StringCalculator();
        assertEquals(0, calculator.add(""));
    }
}
public class StringCalculator {
    public int add(String numbers) {
        return 0; // Hardcoded to pass the test
    }
}

GREEN STATE

public class StringCalculatorTest {
    @Test
    void shouldReturnZeroForEmptyString() {
        StringCalculator calculator = new StringCalculator();
        assertEquals(0, calculator.add(""));
    }
    
    @Test
    void shouldReturnNumberForSingleNumber() {
        StringCalculator calculator = new StringCalculator();
        assertEquals(1, calculator.add("1"));
    }
}

String Calculator Kata

RED STATE

public int add(String numbers) {
    if (numbers.isEmpty()) {
        return 0;
    }
    return Integer.parseInt(numbers);
}
@Test
void shouldReturnNumberForSingleNumber() {
	StringCalculator calculator = new StringCalculator();
	assertEquals(1, calculator.add("1"));
}

String Calculator Kata

GREEN STATE

public int add(String numbers) {
    if (numbers.isEmpty()) {
        return 0;
    }
    return Integer.parseInt(numbers);
}
@Test
void shouldReturnSumOfTwoNumbers() {
    StringCalculator calculator = new StringCalculator();
    assertEquals(3, calculator.add("1,2"));
}

String Calculator Kata

RED STATE

public int add(String numbers) {
    if (numbers.isEmpty()) {
        return 0;
    }
    String[] parts = numbers.split(",");
    int sum = 0;
    for (String part : parts) {
        sum += Integer.parseInt(part);
    }
    return sum;
}
@Test
void shouldReturnSumOfTwoNumbers() {
    StringCalculator calculator = new StringCalculator();
    assertEquals(3, calculator.add("1,2"));
}

String Calculator Kata

GREEN STATE

import java.util.Arrays;

public int add(String numbers) {
    if (numbers.isEmpty()) {
        return 0;
    }
    return Arrays.stream(numbers.split(","))
                 .mapToInt(Integer::parseInt)
                 .sum();
}
@Test
void shouldReturnSumOfTwoNumbers() {
    StringCalculator calculator = new StringCalculator();
    assertEquals(3, calculator.add("1,2"));
}

String Calculator Kata

BLUE STATE

@Test
void shouldReturnSumWithNewlineDelimiter() {
    StringCalculator calculator = new StringCalculator();
    assertEquals(6, calculator.add("1\n2,3"));
}

String Calculator Kata

RED STATE

import java.util.Arrays;

public int add(String numbers) {
    if (numbers.isEmpty()) {
        return 0;
    }
    return Arrays.stream(numbers.split(","))
                 .mapToInt(Integer::parseInt)
                 .sum();
}
@Test
void shouldReturnSumWithNewlineDelimiter() {
    StringCalculator calculator = new StringCalculator();
    assertEquals(6, calculator.add("1\n2,3"));
}

String Calculator Kata

import java.util.Arrays;

public int add(String numbers) {
    if (numbers.isEmpty()) {
        return 0;
    }
    return Arrays.stream(numbers.split(",|\n")) 
                 .mapToInt(Integer::parseInt)
                 .sum();
}

GREEN STATE

@Test
void shouldReturnSumWithCustomDelimiter() {
    StringCalculator calculator = new StringCalculator();
    assertEquals(3, calculator.add("//;\n1;2"));
}

String Calculator Kata

import java.util.Arrays;

public int add(String numbers) {
    if (numbers.isEmpty()) {
        return 0;
    }
    return Arrays.stream(numbers.split(",|\n")) 
                 .mapToInt(Integer::parseInt)
                 .sum();
}

RED STATE

String Calculator Kata

public int add(String numbers) {
    if (numbers.isEmpty()) {
        return 0;
    }
    
    String delimiter = ",|\n";
    String numbersWithoutDelimiter = numbers;

    if (numbers.startsWith("//")) {
        int delimiterIndex = numbers.indexOf("\n");
        // Extract custom delimiter (e.g., ";")
        delimiter = numbers.substring(2, delimiterIndex);
        // Extract the actual numbers part
        numbersWithoutDelimiter = numbers.substring(delimiterIndex + 1);
    }

    return Arrays.stream(numbersWithoutDelimiter.split(delimiter))
                 .mapToInt(Integer::parseInt)
                 .sum();
}

GREEN STATE

String Calculator Kata

public int add(String input) {
  if (input.isEmpty()) { return 0; }

  String delimiter = determineDelimiter(input);
  String numbers = getNumbersWithoutHeader(input);

  return Arrays.stream(numbers.split(delimiter))
  	.mapToInt(Integer::parseInt)
  	.sum();
}

private String determineDelimiter(String input) {
	return input.startsWith("//") 
        ? Pattern.quote(input.substring(2, input.indexOf("\n"))) 
        : ",|\n";
}

private String getNumbersWithoutHeader(String input) {
	return input.startsWith("//") 
        ? input.substring(input.indexOf("\n") + 1) 
        : input;
}

BLUE STATE

Coding Rules for Today

  • Strictly follow TDD rules and do fast cycles

  • Don't forget refactoring

  • As delivery doesn't matter, focus on readable Clean Code

  • Test names and bodies must document the codebase

  • There is no need for external dependencies, mocking, creating UI, etc.

  • Do NOT use AI tools Today

TDD RULZ

Coderetreat

TDD RULZ

Coderetreat

Game Of Life

Game Of Life

  • Any live cell with fewer than two live neighbours dies, as if caused by underpopulation.

  • Any live cell with more than three live neighbours dies, as if by overcrowding.

  • Any live cell with two or three live neighbours lives on to the next generation.

  • Any dead cell with exactly three live neighbours becomes a live cell.

SESSION #1

SESSION #1

No constraint, just do
TDD and Clean Code

SESSION #2

Good tests
document the code

  • Test cases against behaviour

  • Test naming

    • Include the method under testing

    • Add context, preconditions

    • Write what will happen, the expected behaviour

@Test
void add_Given1And2_Returns3() {
  assertEquals(3, add(1, 2));
}
@Test
void add_GivenTwoNumbers_ReturnsSum() {
	assertEquals(1, add(0, 1));
	assertEquals(3, add(1, 2));
}

Bad:

Better:

describe("Math", () => {
	describe("#add", () => {
    	it("should return 3 if 1 and 2 are given", () => {
        	expect(add(1,2)).toEqual(3)
describe("Math", () => {
	describe("#add", () => {
    	it("should return the sum of two numbers given", () => {
        	expect(add(0,1)).toEqual(1)
			expect(add(1,2)).toEqual(3)
@Nested
class GivenBalanceIs1000 { 

  @Nested
  class WhenMakingDepositOf100 { 

    @Test
    void thenExpectBalanceToBe1100() {
      BankAccount account = new BankAccount(1000);
      account.deposit(100);
      assertEquals(1100, account.getBalance());
    }
  }
}

Bad:

Better:

@Nested
class Deposit {

	@Nested
	class GivenInitialBalance {

		@Test
		void addsDepositToBalance() { ... }
	}
}
describe("Given the balance is 1.000 €", () => {
  describe("When making a deposit of 100 €", () => {
    it("Then I expect the balance to be 1.100 €", () => ...
describe("Given an initial balance in the account", () => {
  describe("When making a deposit", () => {
    it("Should add the deposit to the balance", () => ...

Bad:

Better:

SESSION #2

Constraints:

  1. TDD Ping-Pong

  2. Behavior in test names

  3. Bonus: infinite table

SESSION #3

TDD Strategies

Obvious Implementation
Fake It ('til you make it)
Triangulation

TDD Strategies

Obvious Implementation

@Test
void getSquareArea_GivenEdgeLength_ReturnsArea() {
  // Arrange
  int edgeLength = 3;

  // Act
  int area = getSquareArea(edgeLength);

  // Assert
  assertEquals(9, area);
}
int getSquareArea(int edgeLength) {
	return edgeLength * edgeLength;
}

TDD Strategies

Fake it ('til you make it)

@Test
void getSquareArea_GivenEdgeLength_ReturnsArea() {
  assertEquals(9, getSquareArea(3));
}
// Iteration 1: Fake the result
int getSquareArea(int edge) { return 9; }

// Iteration 2: Make it look like a calculation
int getSquareArea(int edge) { return 3 * 3; }

// Iteration 3: Implement the real logic
int getSquareArea(int edge) {
	return edge * edge;
}

TDD Strategies

Triangulation

@Test
void getSquareArea_GivenVariousEdges_ReturnsArea() {
  assertEquals(0, getSquareArea(0));
}

int getSquareArea(int edge) { 
  return 0; 
}
@Test
void getSquareArea_GivenVariousEdges_ReturnsArea() {
  assertEquals(0, getSquareArea(0));
  assertEquals(1, getSquareArea(1));
}

int getSquareArea(int edge) { 
  return edge; 
}
@Test
void getSquareArea_GivenVariousEdges_ReturnsArea() {
  assertEquals(0, getSquareArea(0));
  assertEquals(1, getSquareArea(1));
  assertEquals(4, getSquareArea(2));
}

int getSquareArea(int edge) {
  return edge * edge;
}

SESSION #3

Constraints:

  1. TDD Ping-Pong

  2. Behavior in test names

  3. Inifinite table

  4. Baby Steps

SESSION #4

All About Clean Code

Make your code easy to understand!

All About Clean Code

Make your code easy to understand!

// BAD
int d; // elapsed time in days
public List<int[]> getThem() { ... }


// BETTER
int elapsedTimeInDays;
public List<int[]> getFlaggedCells() { ... }

Use descriptive, unambigious names

All About Clean Code

Make your code easy to understand!

// BAD
double circumference = radius * 2 * 3.14;
if (age >= 18) { ... }


// BETTER
static final double PI = 3.14;
static final int LEGAL_VOTING_AGE = 18;

double circumference = radius * 2 * PI;
if (age >= LEGAL_VOTING_AGE) { ... }

Avoid Magic Numbers and Strings

All About Clean Code

Make your code easy to understand!

// BAD
public void registerUser(User user) {
  if (user.getName() == null) throw new Exception();
  database.save(user);
  emailService.sendWelcome(user.getEmail());
}


// BETTER
public void registerUser(User user) {
  validateUser(user);
  userRepository.save(user);
  emailService.sendWelcomeEmail(user);
}

Functions are Small & Should Do One Thing

All About Clean Code

Make your code easy to understand!

// BAD
public void createMenu(
  String title, 
  String body, 
  String buttonText, 
  boolean cancellable) {
    // ...
}


// BETTER
public void createMenu(MenuConfig config) {
    // ...
}

Limit Function Arguments

All About Clean Code

Make your code easy to understand!

// BAD
// Check to see if the employee is eligible for full benefits
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65)) { ... }


// BETTER
if (employee.isEligibleForFullBenefits()) { ... }

Don't use comments to explain complex code

SESSION #4

Constraints:

  1. First 5 minutes planning!

  2. Follow clean code rules, eg. small functions (max 3 lines)

  3. No primitives (avoid primitive obsession)

  4. Bonus: No loop (for/while)

SESSION #5

SESSION #5

Constraints:

-

SESSION #6