UNC COMP 301 - F26/Lec 9 - Software Testing - JUnit; Design Patterns Introduction
Watch on YouTube →
Overview
Muhammad Sayeed Ghani explains Java checked exceptions, then focuses on unit testing with JUnit: configuring Maven, writing assertion-based tests, measuring coverage, and using test-driven development (TDD) to expose missing requirements. He closes with an introduction to the Gang of Four’s design-pattern categories and the Iterator pattern, which hides collection implementation details.
Key takeaways
- A custom checked exception such as ShortOfFunds can carry both a human-readable message and structured data like a shortfall amount; callers must catch or propagate it.
- Unit, integration, system, and acceptance testing examine progressively broader scopes, while acceptance testing adds real users and varied deployment environments.
- JUnit tests report failure through uncaught exceptions; assertions such as assertEquals both check correctness and provide useful expected-versus-actual diagnostics.
- Coverage reports reveal which classes, methods, lines, and branches tests execute, but high or complete coverage does not prove correctness unless the assertions pass.
- Writing tests against requirements can uncover underspecified behavior before implementation, as the PrimeCounter example revealed that its initial value was never defined.
- The Iterator pattern separates traversal from collection representation, allowing the same client-facing approach to work across arrays, linked lists, trees, and other collections.
Chapters
0:00
Custom Checked Exceptions: ShortOfFunds and Its Shortfall
- A custom ShortOfFunds exception extends Java's checked Exception class, so callers must catch it or declare it with throws.
- Its constructor passes an explanatory message to the superclass and stores the amount of the shortfall in a field.
- A getShortfall() getter lets callers retrieve structured information from the exception, beyond its message.
7:10
Throw Early, Use Specific Exceptions, and Catch When You Can Act
- When an account withdrawal exceeds its balance, withdraw() throws ShortOfFunds with both a message and the shortfall amount.
- A checked exception must be caught or propagated in the method signature; omitting both causes a compiler error.
- Throw exceptions close to the problem, and let them propagate until a layer can resolve them—for example, asking a user to choose a different file.
9:30
Four Levels of Software Testing, from Unit to Acceptance
- Unit testing checks a single method or class; integration testing checks how classes, packages, or modules work together.
- System testing exercises the complete program end to end, while acceptance testing involves users and real-world environments.
- Acceptance tests account for varied operating systems, hardware, memory configurations, other applications, and invalid user input; COMP 301 focuses on unit testing.
12:40
JUnit Setup in Maven and the TDD Idea
- Test-driven development writes tests before implementation, using the software requirements as the starting point.
- JUnit is a Java library for automating unit tests and providing assertion methods.
- Add the JUnit dependency to Maven's pom.xml, then synchronize Maven projects in the IDE to download and resolve it.
15:35
JUnit Test Classes, @Test Methods, and Maven Folder Structure
- Create a separate test class for each production class; a Calculator class, for example, gets a CalculatorTest class.
- Maven keeps tests under a separate test source tree that mirrors the production package structure.
- JUnit 4 is used in the demonstration; test methods are public void methods marked with @Test, with at least one test recommended per method.
20:40
Run a JUnit Test Method or the Entire Test Class
- Running a test class executes all its @Test methods; the demonstrated CalculatorTest run reports 10 tests, with 4 passing and 6 failing.
- Running an individual method, such as testAdd, isolates one test and makes its pass-or-fail result easier to inspect.
- The IDE also offers debugging and coverage runs alongside ordinary test execution.
23:20
JUnit Assertions Turn Expected Results into Test Outcomes
- JUnit test methods do not return pass/fail values; an uncaught exception marks a test as failed, while completing without one marks it as passed.
- A useful test creates the target object, invokes the behavior being checked, and asserts the expected result.
- assertEquals(expected, actual) passes when the values match and fails otherwise; floating-point comparisons need an explicit tolerance or epsilon.
27:00
JUnit Assertion Choices and a Null-Safe Equality Check
- JUnit provides assertTrue, assertFalse, assertEquals, assertNull, assertNotNull, assertSame, assertNotSame, assertArrayEquals, and fail.
- assertEquals compares values using equality semantics; assertSame checks whether two references identify the same object.
- For comparisons, assertEquals gives more diagnostic information than assertTrue(actual == expected) by reporting expected and actual values.
- A custom equality check must handle null explicitly: calling actual.equals(expected) throws NullPointerException when actual is null, even if both inputs are null.
36:30
Focused Tests Isolate Cases and Dependencies
- A distance(a, b) method should return a nonnegative difference regardless of operand order, signs, or whether values are equal.
- Putting four distance scenarios in one test is hard to diagnose: the first failing assertion stops execution, hiding later failures.
- Separate tests identify which input case failed; testing an object's default capacity also depends on its constructor and getter, which should be tested separately.
42:20
JUnit Coverage Reports Lines, Methods, Classes, and Branches
- The IDE's Run with Coverage report measures which classes, methods, lines, and conditional branches execute during tests.
- With four project classes, testing only Grader covers 25% of classes; testing one of its two methods covers 50% of that class's methods.
- A test using 59 points exercises the failing-grade path; adding 60 points tests the pass boundary, and 71 points exercises the C-grade path.
- Coverage can show code that tests have not reached, but 100% coverage alone is insufficient unless the tests also pass.
51:25
TDD Cycles Between Requirements, Tests, and Implementation
- A practical TDD workflow starts with requirements and an interface, adds tests, implements enough code to pass them, then adds further tests and repeats.
- Thinking through tests before coding can expose edge cases and ambiguities in requirements early.
- TDD is iterative rather than necessarily requiring every test to be written before any implementation begins.
55:20
A PrimeCounter Test Reveals an Unspecified Default
- The example interface, PrimeCounter, supports adding a value and checking whether the stored integer is prime.
- A first test asks whether a newly constructed counter contains a prime; assuming its default is zero would imply false, since neither 0 nor 1 is prime.
- The requirements did not specify the initial value, and the constructor accepted no value, so writing the test exposed an incomplete specification that needed clarification.
1:01:00
Expand PrimeCounter Tests and Consider TDD Tradeoffs
- After clarifying the initial value, tests can add 11 and check that it is prime, then extend coverage to negative values and repeated additions and checks.
- New tests and implementation changes continue in cycles, with coverage used to identify untested code and branches.
- TDD can lead developers to optimize narrowly for the tests, and bugs can exist in both implementation and tests even when coverage is high.
- Ghani notes that industry teams may separate implementation and test-writing responsibilities so testers can bring an independent perspective.
1:09:30
Gang of Four Design Patterns: Three Categories
- The 1994 Gang of Four book by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides documented 23 recurring object-oriented design patterns.
- Creational patterns concern object creation; structural patterns concern relationships among objects.
- Behavioral patterns address algorithms, communication, and the distribution of responsibilities between objects.
1:12:40
Iterator Hides Collection Implementations
- The Iterator pattern lets clients traverse a collection without depending on whether it uses an array, linked list, tree, or hash map.
- Instead of exposing indexing details such as an array's for-loop, a common interface provides a uniform way to visit elements.
- The pattern must accommodate different collection sizes and forms, including potentially unbounded sequences such as prime numbers; implementation details are deferred to the next lecture.
Summary, takeaways, and chapters were generated by AI from the video's transcript and may contain errors. The video belongs to its creator, Muhammad Sayeed Ghani.