Skip to content

Debate About Testing Strategies

  1. Roll the dice to pick a topic!
  2. Read both sides of the debate.
  3. Prepare to defend one side, based on the dice roll.
  4. Discuss with your peers!

🎲 1. Instantiating Dependencies (Defend Hardcoded Creation)

Claim to Defend: “Create related objects directly inside your function — just call the constructor you need.”

Three Thinking Questions

  1. If the function creates the object itself, who decides what kind of object it should be — the caller or the callee?
  2. If a function needs a dependency only in certain cases, should it still construct it always?
  3. If object creation is buried deep inside code, how would you replace it when requirements change?

Glossary

  • Dependency: Something your code needs to work.
  • Constructor: Function that creates an object.
  • Hardcoded creation: Making objects directly inside your function.
  • IoC / Dependency Injection: Caller provides the dependency.
  • Test seam: A point where behavior can be swapped for testing.

Code Examples

Python

1
2
3
4
5
6
7
8
9
class TaxCalculator:
    def calculate(self, amount):
        return amount * 0.27

class PaymentService:
    def pay(self, amount):
        tax_calc = TaxCalculator()  # hardcoded creation
        total = amount + tax_calc.calculate(amount)
        return f"Paying {total}"

Java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class TaxCalculator {
    double calculate(double amount) {
        return amount * 0.27;
    }
}

class PaymentService {
    String pay(double amount) {
        TaxCalculator taxCalc = new TaxCalculator(); // hardcoded creation
        double total = amount + taxCalc.calculate(amount);
        return "Paying " + total;
    }
}

🎲 2. Instantiating Dependencies (Against Hardcoded Creation)

Claim to Defend: “Your code shouldn’t create its own dependencies — someone above should hand them in.”

Three Thinking Questions

  1. If the caller provides the dependency, who gains control over choosing different implementations?
  2. How does passing in a dependency change what you can test or fake?
  3. What happens in a large system if dozens of functions create slightly different versions of “the same” object?

Glossary

  • Dependency: External object or service your code needs.
  • Injection: Passing the dependency into a function or constructor.
  • Abstraction: Hiding details so swapping implementations becomes easier.
  • Mock / Fake: Lightweight replacement used in tests.
  • Coupling: How tightly one piece of code depends on another.

Code Examples

Python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class TaxCalculator:
    def calculate(self, amount):
        return amount * 0.27

class PaymentService:
    def __init__(self, tax_calculator):
        self.tax_calculator = tax_calculator  # injected

    def pay(self, amount):
        total = amount + self.tax_calculator.calculate(amount)
        return f"Paying {total}"

Java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class TaxCalculator {
    double calculate(double amount) {
        return amount * 0.27;
    }
}

class PaymentService {
    private final TaxCalculator taxCalc; // injected

    PaymentService(TaxCalculator taxCalc) {
        this.taxCalc = taxCalc;
    }

    String pay(double amount) {
        double total = amount + taxCalc.calculate(amount);
        return "Paying " + total;
    }
}

🎲 3. Global State & Shared Modules (Defend Using Global Settings)

Claim to Defend: “Use global configuration or shared state when multiple parts need the same info.”

Three Thinking Questions

  1. If many places need the same value, is a single global source reducing duplication or increasing risk?
  2. When does centralizing configuration in one global object genuinely make the code simpler for beginners?
  3. If the value rarely changes, is passing it around more confusing than reading it from one shared place?

Glossary

  • Global state: Variables or objects accessible from anywhere.
  • Configuration: Settings that guide program behavior.
  • Shared module: A file where multiple components import the same values.
  • Implicit dependency: Something a function uses without receiving it directly.
  • State mutation: Changing a value during runtime.

Code Examples

Python

1
2
3
4
5
6
7
8
9
# global_settings.py
TAX_RATE = 0.27

# payment_service.py
from global_settings import TAX_RATE

def pay(amount):
    total = amount + amount * TAX_RATE  # reading global
    return f"Paying {total}"

Java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class GlobalConfig {
    public static double TAX_RATE = 0.27;
}

class PaymentService {
    String pay(double amount) {
        double total = amount + amount * GlobalConfig.TAX_RATE; // reading global
        return "Paying " + total;
    }
}

🎲 4. Global State & Shared Modules (Against Using Global Settings)

Claim to Defend: “Relying on global state makes both testing and debugging unpredictable.”

Three Thinking Questions

  1. What happens when two tests modify the same global value at the same time?
  2. How do you reproduce a bug if global state changes depending on the order of execution?
  3. Who is responsible for cleaning up or resetting global values—should every test remember to do it?

Glossary

  • Test contamination: When one test affects another through shared state.
  • Isolation: Ensuring each test runs independently.
  • Determinism: Same input → same output every time.
  • Side effects: Hidden changes caused by running a function.
  • State reset: Restoring globals after use.

Code Examples

Python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# global_settings.py
TAX_RATE = 0.27

def set_tax_rate(new_rate):
    global TAX_RATE
    TAX_RATE = new_rate  # mutation creates test risk

# test_payment.py
from global_settings import set_tax_rate
from payment_service import pay

set_tax_rate(0.10)      # test modifies global
result = pay(100)       # other tests may see this change

Java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class GlobalConfig {
    public static double TAX_RATE = 0.27;
}

class Tests {
    void testPayment() {
        GlobalConfig.TAX_RATE = 0.10; // mutation creates test risk
        // Other tests may read this value accidentally
    }
}

🎲 5. Static Helpers & Module Functions (Defend Static-Style Utilities)

Claim to Defend: “Use helper functions or class-level utilities instead of passing objects around.”

Three Thinking Questions

  1. When does calling a static helper directly make code shorter and easier to read than wiring objects everywhere?
  2. If a helper has no state, is creating an object for it unnecessary ceremony?
  3. Do teams sometimes overuse dependency injection when a simple function call would do?

Glossary

  • Static helper: A function or method that can be used without creating an object.
  • Utility module/class: A collection of stateless helper functions.
  • Boilerplate: Extra code needed just to satisfy architecture rules.
  • Stateless operation: A function whose result depends only on its input.
  • Direct call: Calling a function without passing an object around.

Code Examples

Python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# utils.py
def add_tax(amount):
    return amount * 1.27

# payment.py
from utils import add_tax

def pay(amount):
    total = add_tax(amount)  # static-style helper
    return f"Paying {total}"

Java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Utils {
    public static double addTax(double amount) {
        return amount * 1.27;
    }
}

class PaymentService {
    String pay(double amount) {
        double total = Utils.addTax(amount); // static helper
        return "Paying " + total;
    }
}

🎲 6. Static Helpers & Module Functions (Against Static-Style Utilities)

Claim to Defend: “Static-style helpers lock down behavior and make it impossible to replace in tests.”

Three Thinking Questions

  1. If a static call is everywhere, how do you swap its behavior for testing without editing the entire codebase?
  2. How do you introduce a different tax rule for one specific scenario if every call goes through the same static function?
  3. Should low-level utilities decide behavior, or should callers choose what logic they need?

Glossary

  • Interceptability: Ability to replace or fake a behavior during tests.
  • Seam: A place where behavior can be changed without editing code.
  • Hard dependency: A dependency that cannot be swapped or overridden.
  • Test double: A placeholder (fake, stub, mock) used in tests.
  • Behavior injection: Passing in the logic instead of using a global/static one.

Code Examples

Python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# utils.py
def add_tax(amount):
    return amount * 1.27  # hard to replace in tests

# payment.py
from utils import add_tax

def pay(amount, tax_fn=add_tax):  # inject behavior instead
    total = tax_fn(amount)
    return f"Paying {total}"

Java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Utils {
    public static double addTax(double amount) {
        return amount * 1.27; // fixed behavior
    }
}

class PaymentService {
    private final java.util.function.DoubleUnaryOperator taxFn;

    PaymentService(java.util.function.DoubleUnaryOperator taxFn) {
        this.taxFn = taxFn; // injected behavior
    }

    String pay(double amount) {
        double total = taxFn.applyAsDouble(amount);
        return "Paying " + total;
    }
}

🎲 7. Scenario/Flow Tests (Defend Focusing on Scenario Tests)

Claim to Defend: “Focus on scenario tests and skip small unit tests.”

Three Thinking Questions

  1. If the whole system works end‑to‑end, do we really need small tests checking every tiny detail?
  2. When debugging, is it sometimes clearer to see the full story instead of many isolated pieces?
  3. In a small student project, can writing lots of unit tests slow the team down more than it helps?

Glossary

  • Scenario test / flow test: A test that checks the whole system or a complete user action.
  • End‑to‑end (E2E): Running the whole flow through real components.
  • Happy path: The scenario where everything works as expected.
  • Regression: A bug that comes back after being fixed.
  • Overtesting: Adding too many small tests that give little value.

Code Examples

Python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# booking_system.py
def book_ticket(user, price):
    if user.balance >= price:
        user.balance -= price
        return "OK"
    return "FAIL"

# test_flow.py
def test_booking_flow():
    user = type("U", (), {"balance": 100})()  # simple dummy object
    result = book_ticket(user, 50)
    assert result == "OK"
    assert user.balance == 50

Java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class User {
    public int balance;
    User(int balance) { this.balance = balance; }
}

class Booking {
    static String book(User user, int price) {
        if (user.balance >= price) {
            user.balance -= price;
            return "OK";
        }
        return "FAIL";
    }
}

class FlowTest {
    void testBookingFlow() {
        User user = new User(100);
        String result = Booking.book(user, 50);
        assert result.equals("OK");
        assert user.balance == 50;
    }
}

🎲 8. Scenario/Flow Tests (Against Relying Only on Scenario Tests)

Claim to Defend: “Flow tests alone are fragile and slow — unit tests are necessary.”

Three Thinking Questions

  1. When a flow test fails, how do you know which part broke?
  2. If a single small bug blocks the whole flow, is this helpful feedback or noise?
  3. Are you confident that all edge cases appear in one big scenario test?

Glossary

  • Unit test: A small test for a small piece of logic.
  • Fragile test: A test that fails for unrelated reasons.
  • Slow feedback: When tests take too long to run, so developers avoid running them.
  • Isolation: Testing small units independently.
  • Debugging surface: How much code you must inspect when something fails.

Code Examples

Python

1
2
3
4
5
6
7
# price.py
def add_tax(amount):
    return amount * 1.27

# test_price.py
def test_add_tax():
    assert add_tax(100) == 127  # small, focused, reliable

Java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Price {
    static double addTax(double amount) {
        return amount * 1.27;
    }
}

class PriceTest {
    @org.junit.Test
    public void testAddTax() {
        assert Price.addTax(100) == 127.0;
    }
}

🎲 9. Inheritance vs Composition (Defend Subclassing)

Claim to Defend: “Subclass an existing class to add behavior quickly.”

Three Thinking Questions

  1. When does extending an existing class save time compared to writing a new one from scratch?
  2. If the parent class already solves 80% of your problem, isn’t extending it simpler than composing pieces?
  3. Is duplication worse than relying on a base class with some quirks?

Glossary

  • Inheritance: Creating a new class based on another class.
  • Subclass: A class that extends another (the parent).
  • Overriding: Replacing a parent method with a new version.
  • Code reuse: Using existing logic instead of rewriting it.
  • Hierarchy: A class structure formed through inheritance.

Code Examples

Python

1
2
3
4
5
6
7
8
class Logger:
    def log(self, msg):
        print("LOG:", msg)

class FileLogger(Logger):  # subclassing
    def log(self, msg):
        with open("out.txt", "a") as f:
            f.write(msg + "\n")

Java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Logger {
    void log(String msg) {
        System.out.println("LOG: " + msg);
    }
}

class FileLogger extends Logger {  // subclassing
    @Override
    void log(String msg) {
        // write to file (simplified)
        System.out.println("Writing to file: " + msg);
    }
}

🎲 10. Inheritance vs Composition (Against Subclassing)

Claim to Defend: “Subclassing ties you to hidden behavior and makes the code harder to test.”

Three Thinking Questions

  1. How do you know what hidden behavior you might be inheriting from the parent class?
  2. What happens when the parent class changes — do all children silently change as well?
  3. Can you test your subclass in isolation if the parent class does complex things?

Glossary

  • Composition: Building behavior by combining objects rather than extending classes.
  • Coupling: How tightly one class depends on another.
  • Hidden behavior: Logic in the parent class that affects the child unexpectedly.
  • Surface area: The total set of behaviors a class exposes.
  • Fragile base class: A parent class change breaks child classes.

Code Examples

Python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class FileWriter:
    def write(self, msg):
        with open("out.txt", "a") as f:
            f.write(msg + "\n")

class Logger:
    def __init__(self, writer):
        self.writer = writer  # composition

    def log(self, msg):
        self.writer.write(msg)

Java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class FileWriter {
    void write(String msg) {
        System.out.println("Writing to file: " + msg);
    }
}

class Logger {
    private final FileWriter writer; // composition

    Logger(FileWriter writer) {
        this.writer = writer;
    }

    void log(String msg) {
        writer.write(msg);
    }
}

🎲 11. Time-Based Logic (Defend Direct Time Access)

Claim to Defend: “Call the current time directly wherever you need it.”

Three Thinking Questions

  1. When is calling now() simply the clearest and most natural way to express time-based behavior?
  2. If a feature depends on the real current time, is abstraction unnecessary overhead?
  3. Do small projects actually benefit from adding a clock object, or is it architectural noise?

Glossary

  • System time: The real current time from the operating system.
  • Timestamp: A numeric or string representation of a moment.
  • Inline dependency: A dependency used directly inside the function.
  • Real-time logic: Code that depends on the actual moment of execution.
  • Pragmatism: Choosing the simplest approach that works.

Code Examples

Python

1
2
3
4
5
import datetime

def is_store_open():
    now = datetime.datetime.now()  # direct time call
    return 9 <= now.hour < 17

Java

1
2
3
4
5
6
7
8
import java.time.LocalTime;

class Store {
    static boolean isOpen() {
        LocalTime now = LocalTime.now(); // direct time call
        return now.getHour() >= 9 && now.getHour() < 17;
    }
}

🎲 12. Time-Based Logic (Against Direct Time Access)

Claim to Defend: “Direct time access makes behavior unpredictable and tests flaky.”

Three Thinking Questions

  1. How do you test code that depends on the exact current time without mocking or injecting a clock?
  2. What happens when a test runs at 16:59 one day and 17:01 the next?
  3. Should business logic depend on when the test suite happens to run?

Glossary

  • Flaky test: A test that sometimes passes, sometimes fails, without code changes.
  • Determinism: Same input → same output every time.
  • Clock injection: Passing the time source into the function.
  • Predictability: Behavior remains stable over time.
  • Temporal dependency: A dependency on the moment the code executes.

Code Examples

Python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def is_store_open(now_fn):
    now = now_fn()  # injected time source
    return 9 <= now.hour < 17

# test
def fake_now():
    class T: hour = 10
    return T()

assert is_store_open(fake_now) is True

Java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import java.time.LocalTime;
import java.util.function.Supplier;

class Store {
    private final Supplier<LocalTime> clock; // injected time source

    Store(Supplier<LocalTime> clock) {
        this.clock = clock;
    }

    boolean isOpen() {
        LocalTime now = clock.get();
        return now.getHour() >= 9 && now.getHour() < 17;
    }
}