Series: Software Engineering Fundamentals | Part Part 15 of 19 > Delivered at Universidade Potiguar (UnP) in 2010
In this lecture, we built on our previous exploration of TDD by diving deeper into unit testing, using JUnit to demonstrate how structured validation turns vague logic into predictable behavior. It was not just about testing—it was about creating feedback loops, improving design, and building safer systems with fewer surprises.
I wanted the class to realize: testing early isn’t just defensive programming. It’s a way to guide development through intent.
Before Frameworks: Raw Testing and Its Limits
To begin, we returned to a time before JUnit. I demonstrated how a method that calculates square roots can be tested using plain Java. We used this Calculadora
class:
public final class Calculadora {
public static int qualARaiz(int x) {
int guess = 1;
while (guess * guess < x) {
guess++;
}
return guess;
}
}
And tested it manually:
public static void main(String[] args) {
System.out.println(Calculadora.qualARaiz(0));
System.out.println(Calculadora.qualARaiz(9));
System.out.println(Calculadora.qualARaiz(100));
}
The problem? There’s no way to automatically detect failures or track test results over time. It’s fragile, manual, and quickly becomes a bottleneck.
Enter JUnit: Naming, Fixtures, and Automation
We then introduced JUnit—the Java testing framework co-created by Kent Beck. We defined the core components:
- Fixture: setup data used in tests
- Test Case: a single method validation
- Test Suite: collection of test cases
- Test Runner: tool to execute and report
The class wrote its first annotated test:
@Test
public void testCalculateRoot() {
assertEquals(3, Calculadora.qualARaiz(9));
assertEquals(10, Calculadora.qualARaiz(100));
}
We discussed the importance of naming and structure. When tests describe behavior and fail clearly, they become executable documentation.
Building Examples with Arithmetic and Conditions
We added new business logic using the Aritmetica
class:
public class Aritmetica {
public static int soma(int i, int j) {
return i + j;
}
public static boolean isPositivo(int numero) {
return numero > 0;
}
}
And tested it with:
@Test
public void testAddition() {
assertEquals(4, Aritmetica.soma(2,2));
assertEquals(-15, Aritmetica.soma(-10, -5));
}
@Test
public void testIsPositive() {
assertTrue(Aritmetica.isPositivo(5));
assertFalse(Aritmetica.isPositivo(-10));
}
Here, students learned that a single responsibility in a method makes it easier to validate. And booleans can’t lie—tests reveal logic flaws immediately.
Modeling Behavior with the Counter Class
In the second half of the class, we introduced a practical modeling example: a Contador
used in a queue system.
public class Contador {
private int count = 0;
public int increment() {
return ++count;
}
public int decrement() {
return --count;
}
}
And its corresponding tests:
@Before
public void setUp() {
counter = new Contador();
}
@Test
public void testIncrement() {
assertEquals(1, counter.increment());
assertEquals(2, counter.increment());
}
@Test
public void testDecrement() {
assertEquals(-1, counter.decrement());
}
Students practiced creating tests that maintain state across executions. The setUp method was introduced to isolate test logic from instantiation noise.
Activities and Takeaways
We ended with a group challenge: implement and test a class for validating voting eligibility based on age and citizenship. Each team had to define:
- What a valid user looks like
- Which rules apply
- How to test edge cases and constraints
Through peer review, students saw the difference between testing logic and behavior. They gained insight on how test structure improves maintainability and collaboration.
Any team or facilitator can replicate this lesson in onboarding or skill growth sessions. Just start with a simple class, define behaviors, and test intentionally.
Posted as part of the Software Engineering course journal. Today we learned that unit testing with JUnit isn’t just about catching bugs—it’s about building systems that communicate their intent clearly and evolve safely.
Series Navigation
- Introduction: Part 1 - Why Software Engineering?
- Previous: Part 14 - Test-Driven Development
- Next: Part 16 - Advanced TDD: Thinking in Tests
- Current: Part 15 - Unit Testing with JUnit
- Complete series: Why Software Engineering? | Taming Complexity | Waterfall Model | Evolutionary Models | Agile Mindset | Scrum Productivity | Scrum Cycle | XP Quality & Courage | XP Principles & Practices | XP in Practice | Domain-Driven Design | Requirements & Testing | Software Testing | Test-Driven Development | Unit Testing with JUnit | Advanced TDD: Thinking in Tests