Independent Tests and Mocks¶
A mistake in one student member should fail only the tests of that member. This page shows how to create objects without the student's constructor, replace single student methods with test code, and check that a method was called, so failures do not cascade from one member to the next.
Why tests cascade¶
Two situations make failures cascade. In the first, the method under test calls another student method: if that method is wrong, the
test fails too. In the second, the test needs an object of a student class only as a value, but
newObj() runs the student's constructor. Three tools remove these dependencies. All three run no
constructor of the student class, so a missing or broken constructor cannot fail the test.
| Call | Result |
|---|---|
newObjWithoutConstructor() |
an object of the class whose attributes keep their default values (0, false, null; field initialisers do not run either). Concrete classes, also final ones, keep their exact class. Abstract classes and interfaces get a generated subclass. |
newObjOverriding(Map.of(method, args -> result, ...)) |
an object of a generated subclass in which each given method returns the result of its function |
mock() |
a Mock: overrides like newObjOverriding, plus a record of every call |
A method under test calls another student method¶
Example (SE01E03): isShowReady() calls getNumberOfSpotlights(). Replace the called method, so that only isShowReady is tested:
Object stage = stageWrapper.newObjOverriding(Map.of(stageWrapper.getNumberOfSpotlights(), args -> 3));
assertThat(stageWrapper.isShowReady().invokeOnSpecificObject(stage)).isTrue();
The student's isShowReady() runs unchanged. Its call this.getNumberOfSpotlights() (or
getNumberOfSpotlights()) goes to the test's function.
The method under test must call a method of another object¶
Example (SE01E04): Drummer.calculateLoudness(volume, crowdNoise) must use
guitarist.calculateLoudness(volume). Give the Guitarist a return value the student
cannot produce by inlining volume * 10, and check the call:
Mock<?> guitarist = guitaristWrapper.mock()
.override(guitaristWrapper.calculateLoudness(), args -> (int) args[0] * 7);
Object drummer = drummerWrapper.newObjWithoutConstructor();
drummerWrapper.guitarist().setValue(drummer, guitarist.obj());
assertThat(drummerWrapper.calculateLoudness().invokeOnSpecificObject(drummer, 5, 3)).isEqualTo(38);
guitarist.assertCalled(guitaristWrapper.calculateLoudness(), 1,
() -> "Drummer.calculateLoudness(5, 3) must use calculateLoudness of its guitarist.");
An object is needed only as a value¶
The Drummer's accessor tests need some Guitarist, but not a correct constructor of it:
Object guitarist = guitaristWrapper.newObjWithoutConstructor(); // no Guitarist constructor runs
Object drummer = drummerWrapper.newObj();
drummerWrapper.setGuitarist().invokeOnSpecificObject(drummer, guitarist);
Mock in detail¶
| Call | Effect |
|---|---|
override(method, args -> result) |
method runs the function. |
returning(method, value) |
method always returns value. |
othersReturnDefaults() |
Methods without an override return 0, false or null instead of running the student's code (a full mock). equals, hashCode and toString without an override behave like those of Object (identity), so the object still works in lists, sets and messages. |
obj() |
Returns the object. It is created on the first call and stays the same; overrides added later apply to it as well. |
callCount(method) |
Returns the number of calls. |
calls(method) |
Returns the arguments of the recorded calls, in call order: the first 1000 calls per method (Mock.MAX_RECORDED_CALLS). Arrays are copied when the call is recorded; other objects are the ones passed. |
assertCalled(method, times[, context]), assertNotCalled(method) |
Fails with the expected and actual number of calls and the recorded arguments, e.g. Calls: (5), (7). |
resetCalls() |
Forgets all recorded calls, e.g. after preparing the object. |
- Methods are matched by the method the wrapper actually found, so a misspelt but tolerated name
(
calculateLoudnes) is still overridden. AMethodWrapperof a superclass or interface wrapper overrides the inherited method. Of an overloaded method, only the matching overload is overridden. Package-private methods can be overridden, too: the subclass is defined in the student's own package and class loader. - The function gets the arguments in the parameter types the wrapper expects, e.g. an
Integerforint, even if the student declaredlong. Its result is converted to the student's return type (numeric types are converted, e.g.args -> 7for adoublemethod). If the function throws, or returnsnullfor a primitive return type, the call fails like a call that throws. - Methods without an override run the student's code, supervised by Ares. An abstract method without an override fails with "The abstract method ... was called, but the test does not override it."
- The functions are test code, so Ares does not restrict them. They are called from student code, though, so keep them to plain computations.
- A missing class or method fails with the usual
MISSINGmessage. A class or method that cannot be overridden fails with a readable message, e.g.Guitarist: cannot be overridden (final).calls,callCount,assertCalledandassertNotCalledcheck the method in the same way, because calls of afinal,staticorprivatemethod cannot be counted. - Only
mock()records calls. The objects ofnewObjOverridingandnewObjWithoutConstructordo not, since nobody can ask for them. A clone of a mock object (super.clone()) shares the mock: its calls are recorded there, and methods without an override run on the clone.
Limits¶
These cannot be replaced, and the call fails readably instead:
finalclasses and methods, records, enums, sealed classes;staticandprivatemethods;- constructors.
Two more limits concern code that bypasses the methods:
- An attribute that student code reads directly is not affected by any override. Set it with
attribute.setValue(obj, value). - A generated subclass is a different class, so a student's
getClass()-basedequalsreturnsfalseagainst a real object.
There are no argument matchers and no call-order checks. A function and calls(method) cover these.