Debate About Testing Strategies
- Roll the dice to pick a topic!
- Read both sides of the debate.
- Prepare to defend one side, based on the dice roll.
- 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
- If the function creates the object itself, who decides what kind of object it should be — the caller or the callee?
- If a function needs a dependency only in certain cases, should it still construct it always?
- 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
| 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
- If the caller provides the dependency, who gains control over choosing different implementations?
- How does passing in a dependency change what you can test or fake?
- 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
| 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
- If many places need the same value, is a single global source reducing duplication or increasing risk?
- When does centralizing configuration in one global object genuinely make the code simpler for beginners?
- 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
| # 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
| 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
- What happens when two tests modify the same global value at the same time?
- How do you reproduce a bug if global state changes depending on the order of execution?
- 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
| 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
- When does calling a static helper directly make code shorter and easier to read than wiring objects everywhere?
- If a helper has no state, is creating an object for it unnecessary ceremony?
- 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
| # 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
- If a static call is everywhere, how do you swap its behavior for testing without editing the entire codebase?
- How do you introduce a different tax rule for one specific scenario if every call goes through the same static function?
- 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
| # 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
- If the whole system works end‑to‑end, do we really need small tests checking every tiny detail?
- When debugging, is it sometimes clearer to see the full story instead of many isolated pieces?
- 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
- When a flow test fails, how do you know which part broke?
- If a single small bug blocks the whole flow, is this helpful feedback or noise?
- 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
| # 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
- When does extending an existing class save time compared to writing a new one from scratch?
- If the parent class already solves 80% of your problem, isn’t extending it simpler than composing pieces?
- 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
| 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
- How do you know what hidden behavior you might be inheriting from the parent class?
- What happens when the parent class changes — do all children silently change as well?
- 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
| 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
- When is calling
now() simply the clearest and most natural way to express time-based behavior?
- If a feature depends on the real current time, is abstraction unnecessary overhead?
- 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
| import datetime
def is_store_open():
now = datetime.datetime.now() # direct time call
return 9 <= now.hour < 17
|
Java
| 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
- How do you test code that depends on the exact current time without mocking or injecting a clock?
- What happens when a test runs at 16:59 one day and 17:01 the next?
- 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
| 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;
}
}
|