Testing (course memo 1 & 2)
Java, JUnit.
Using a library / package manager recap
A framework is a type of library; a library is software meant to be used by other software (e.g. a testing library so you don't have to build test tooling yourself). Before package managers, you'd manually download a library into your project — cumbersome and insecure, unmanageable once a project depends on tens/hundreds of libraries.
Maven is Java's package manager: running mvn (e.g. via IntelliJ's "Fix" prompt for a missing package) fetches the requested library from Maven Central, verifying it's the authentic official artifact (anti-tampering checks) rather than downloading from some random source.
Where tests live
Right-click the class to test → Generate → Test (IntelliJ offers to install JUnit if missing).
- The class being tested is the production class.
- The class containing its tests is the test class, placed in the same package as the production class, named
<ProductionClassName>Test.
public class Exaggerator {
}class ExaggeratorTest {
}What to test (course rules for this module)
- Write tests for all public methods of a service class that have a return value and at least one argument.
- Don't test: the
mainmethod, constructors,hashCode/equalsdirectly, getters/setters directly, code that reads input viaScanner, or code that uses randomness (instead, isolate the testable parts and test those). - In real projects, deciding what to test is nuanced/judgment-based — this module deliberately asks for more test coverage than typical, for practice.
Default (package-private) visibility
No modifier at all = default visibility: visible only to classes in the same package. Useful for testing — a method with real logic that you want to keep out of the public API, but still need to test, can be given default (not private) visibility so the test class (same package) can call it.
Test structure (Arrange-Act-Assert)
public class Exaggerator {
public String exaggerate(String text) {
return text + "!";
}
}import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class ExaggeratorTest {
Exaggerator exaggerator = new Exaggerator();
@Test
void exaggerate() {
String text = "ice-cream";
String result = exaggerator.exaggerate(text);
String expected = "ice-cream!";
Assertions.assertEquals(expected, result);
}
}- Test class/methods/attributes can use default visibility; class name in PascalCase, named
<ProductionClass>Test. - Production class under test is held as an attribute of the test class.
- One test method per production method with real logic, annotated
@Testso JUnit finds and runs it automatically. - Three stages per test: preparation (build the input), execution (call the method under test, capture the result), assertion (compare result to expected via
Assertions.assertEquals(expected, result)etc).
Parameterized tests
Repeating the same test body with only the input/expected values changing is a signal to automate the automation — a parameterized test.
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
@ParameterizedTest
@CsvSource({
"!, ''",
"!!, !",
"a!, a"
})
void exaggerate(String expected, String text) {
String result = exaggerator.exaggerate(text);
Assertions.assertEquals(expected, result);
}@ParameterizedTestreplaces@Testso JUnit knows to run it once per parameter group.@CsvSourcesupplies the data: each string in the array is one parameter group; within a group, comma-separated values are the individual parameters, mapped positionally to the test method's arguments.- An explicit empty string is written as
''. - The test runs once per parameter group provided.
Test-Driven Development (TDD)
Write the test before the production code, and write only enough production code to satisfy the test. Three laws:
- No production code until a failing unit test exists.
- Write no more of a test than enough to fail (not compiling counts as failing).
- Write no more production code than enough to pass the currently failing test.
Result: very short (~30s) red/green cycles switching between test and production code. TDD has drawn increasing criticism over the years and isn't a fit for every situation.
F.I.R.S.T. principles for good tests
Since test code often ends up as large as (or larger than) production code, it deserves the same care:
- Fast — slow tests don't get run regularly, and both test and production code rot.
- Independent — no test depends on another; run order shouldn't matter.
- Repeatable — same result in any environment (prod/test/local); flaky-per-environment tests aren't trustworthy.
- Self-validating — a test's outcome is a clear pass/fail, nothing to manually inspect.
- Timely — written just before the production code that makes them pass; writing them long after tends to produce production code that's hard to test.
Test coverage
The percentage of production code lines/branches actually exercised by tests, used to catch code nobody's testing. IntelliJ has a built-in coverage tool.