• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hazendaz / jmockit1 / 548

29 Nov 2025 11:21PM UTC coverage: 72.124% (-0.08%) from 72.2%
548

Pull #424

github

hazendaz
[junit5] Add new annotation 'ExpectedException'

This annotation enables JUnit 5 tests to specify expected JMockit exceptions (such as MissingInvocation or UnexpectedInvocation or any other) directly on the test method, mirroring the JUnit 4 @test(expected = ...) pattern or ExpectedException rule usage. It simplifies migration of JMockit-based tests from JUnit 4 to JUnit 5, ensuring consistent exception verification for mocking scenarios.

For standard exception assertions unrelated to JMockit, users should continue to use JUnit 5’s assertThrows.

In beforeTestExecution, setup issues will trigger a TestAbortedException when expected match without failing test or attempting to run internals.

handle test execution exception and after test execution will check if expected exception and suppress the exception on match.

Expected exception message can be passed following junit 4 style of contains, if exact match is needed you can disable the messageContains flag
Pull Request #424: Introduction of 'ExpectedException' annotation

5687 of 8390 branches covered (67.78%)

Branch coverage included in aggregate %.

22 of 30 new or added lines in 1 file covered. (73.33%)

14 existing lines in 3 files now uncovered.

11938 of 16047 relevant lines covered (74.39%)

0.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

59.75
/main/src/main/java/mockit/integration/junit5/JMockitExtension.java
1
/*
2
 * MIT License
3
 * Copyright (c) 2006-2025 JMockit developers
4
 * See LICENSE file for full license text.
5
 */
6
package mockit.integration.junit5;
7

8
import edu.umd.cs.findbugs.annotations.NonNull;
9
import edu.umd.cs.findbugs.annotations.Nullable;
10

11
import java.lang.reflect.Method;
12
import java.util.Arrays;
13
import java.util.stream.Collectors;
14

15
import mockit.Capturing;
16
import mockit.Injectable;
17
import mockit.Mocked;
18
import mockit.Tested;
19
import mockit.integration.TestRunnerDecorator;
20
import mockit.internal.expectations.RecordAndReplayExecution;
21
import mockit.internal.state.SavePoint;
22
import mockit.internal.state.TestRun;
23
import mockit.internal.util.StackTrace;
24
import mockit.internal.util.Utilities;
25

26
import org.junit.jupiter.api.BeforeAll;
27
import org.junit.jupiter.api.BeforeEach;
28
import org.junit.jupiter.api.Nested;
29
import org.junit.jupiter.api.extension.AfterAllCallback;
30
import org.junit.jupiter.api.extension.AfterEachCallback;
31
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
32
import org.junit.jupiter.api.extension.BeforeAllCallback;
33
import org.junit.jupiter.api.extension.BeforeEachCallback;
34
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
35
import org.junit.jupiter.api.extension.ExtensionContext;
36
import org.junit.jupiter.api.extension.ParameterContext;
37
import org.junit.jupiter.api.extension.ParameterResolver;
38
import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;
39
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
40
import org.opentest4j.TestAbortedException;
41

42
public final class JMockitExtension extends TestRunnerDecorator implements BeforeAllCallback, AfterAllCallback,
1✔
43
        TestInstancePostProcessor, BeforeEachCallback, AfterEachCallback, BeforeTestExecutionCallback,
44
        AfterTestExecutionCallback, ParameterResolver, TestExecutionExceptionHandler {
45
    @Nullable
46
    private SavePoint savePointForTestClass;
47
    @Nullable
48
    private SavePoint savePointForTest;
49
    @Nullable
50
    private SavePoint savePointForTestMethod;
51
    @Nullable
52
    private Throwable thrownByTest;
53
    private Object[] parameterValues;
54
    private ParamValueInitContext initContext = new ParamValueInitContext(null, null, null,
1✔
55
            "No callbacks have been processed, preventing parameter population");
56

57
    @Override
58
    public void beforeAll(@NonNull ExtensionContext context) {
59
        if (!isRegularTestClass(context)) {
1!
60
            return;
×
61
        }
62

63
        @Nullable
64
        Class<?> testClass = context.getTestClass().orElse(null);
1✔
65
        savePointForTestClass = new SavePoint();
1✔
66
        // Ensure JMockit state and test class logic is handled before any test instance is created
67
        if (testClass != null) {
1!
68
            updateTestClassState(null, testClass);
1✔
69
        }
70

71
        if (testClass == null) {
1!
72
            initContext = new ParamValueInitContext(null, null, null,
×
73
                    "@BeforeAll setup failed to acquire 'Class' of test");
74
            return;
×
75
        }
76

77
        // @BeforeAll can be used on instance methods depending on @TestInstance(PER_CLASS) usage
78
        Object testInstance = context.getTestInstance().orElse(null);
1✔
79
        Method beforeAllMethod = Utilities.getAnnotatedDeclaredMethod(testClass, BeforeAll.class);
1✔
80
        if (testInstance == null) {
1!
81
            initContext = new ParamValueInitContext(null, testClass, beforeAllMethod,
1✔
82
                    "@BeforeAll setup failed to acquire instance of test class");
83
            return;
1✔
84
        }
85

86
        if (beforeAllMethod != null) {
×
87
            initContext = new ParamValueInitContext(testInstance, testClass, beforeAllMethod, null);
×
88
            parameterValues = createInstancesForAnnotatedParameters(testInstance, beforeAllMethod, null);
×
89
        }
90
    }
×
91

92
    private static boolean isRegularTestClass(@NonNull ExtensionContext context) {
93
        Class<?> testClass = context.getTestClass().orElse(null);
1✔
94
        return testClass != null && !testClass.isAnnotationPresent(Nested.class);
1!
95
    }
96

97
    @Override
98
    public void postProcessTestInstance(@NonNull Object testInstance, @NonNull ExtensionContext context) {
99
        if (!isRegularTestClass(context)) {
1!
100
            return;
×
101
        }
102

103
        TestRun.enterNoMockingZone();
1✔
104

105
        try {
106
            handleMockFieldsForWholeTestClass(testInstance);
1✔
107
        } finally {
108
            TestRun.exitNoMockingZone();
1✔
109
        }
110

111
        TestRun.setRunningIndividualTest(testInstance);
1✔
112
    }
1✔
113

114
    @Override
115
    public void beforeEach(@NonNull ExtensionContext context) {
116
        Object testInstance = context.getTestInstance().orElse(null);
1✔
117
        Class<?> testClass = context.getTestClass().orElse(null);
1✔
118
        if (testInstance == null) {
1!
119
            initContext = new ParamValueInitContext(null, null, null,
×
120
                    "@BeforeEach setup failed to acquire instance of test class");
121
            return;
×
122
        }
123

124
        TestRun.prepareForNextTest();
1✔
125
        TestRun.enterNoMockingZone();
1✔
126

127
        try {
128
            savePointForTest = new SavePoint();
1✔
129
            createInstancesForTestedFieldsBeforeSetup(testInstance);
1✔
130

131
            if (testClass == null) {
1!
132
                initContext = new ParamValueInitContext(null, null, null,
×
133
                        "@BeforeEach setup failed to acquire Class<?> of test");
134
                return;
×
135
            }
136

137
            Method beforeEachMethod = Utilities.getAnnotatedDeclaredMethod(testClass, BeforeEach.class);
1✔
138
            if (beforeEachMethod != null) {
1✔
139
                initContext = new ParamValueInitContext(testInstance, testClass, beforeEachMethod, null);
1✔
140
                parameterValues = createInstancesForAnnotatedParameters(testInstance, beforeEachMethod, null);
1✔
141
            }
142
        } finally {
143
            TestRun.exitNoMockingZone();
1✔
144
        }
145
    }
1✔
146

147
    @Override
148
    public void beforeTestExecution(@NonNull ExtensionContext context) {
149
        Class<?> testClass = context.getTestClass().orElse(null);
1✔
150
        Method testMethod = context.getTestMethod().orElse(null);
1✔
151
        Object testInstance = context.getTestInstance().orElse(null);
1✔
152

153
        if (testMethod == null || testInstance == null) {
1!
154
            initContext = new ParamValueInitContext(testInstance, testClass, testMethod,
×
155
                    "@Test failed to acquire instance of test class, or target method");
156
            return;
×
157
        }
158

159
        TestRun.enterNoMockingZone();
1✔
160

161
        try {
162
            savePointForTestMethod = new SavePoint();
1✔
163
            createInstancesForTestedFieldsFromBaseClasses(testInstance);
1✔
164
            initContext = new ParamValueInitContext(testInstance, testClass, testMethod, null);
1✔
165
            parameterValues = createInstancesForAnnotatedParameters(testInstance, testMethod, null);
1✔
166
            createInstancesForTestedFields(testInstance);
1✔
167
        } catch (Throwable e) {
1✔
168
            if (isExpectedException(context, e)) {
1!
169
                throw new TestAbortedException("Expected exception occurred in setup: " + e.getMessage());
1✔
170
            }
NEW
171
            throw e;
×
172
        } finally {
173
            TestRun.exitNoMockingZone();
1✔
174
        }
175

176
        TestRun.setRunningIndividualTest(testInstance);
1✔
177
    }
1✔
178

179
    @Override
180
    public boolean supportsParameter(@NonNull ParameterContext parameterContext,
181
            @NonNull ExtensionContext extensionContext) {
182
        return parameterContext.isAnnotated(Tested.class) || parameterContext.isAnnotated(Mocked.class)
1✔
183
                || parameterContext.isAnnotated(Injectable.class) || parameterContext.isAnnotated(Capturing.class);
1!
184
    }
185

186
    @Override
187
    public Object resolveParameter(@NonNull ParameterContext parameterContext,
188
            @NonNull ExtensionContext extensionContext) {
189
        int parameterIndex = parameterContext.getIndex();
1✔
190
        if (parameterValues == null) {
1!
191
            String warning = initContext.warning;
×
192
            StringBuilder exceptionMessage = new StringBuilder(
×
193
                    "JMockit failed to provide parameters to JUnit 5 ParameterResolver.");
194
            if (warning != null) {
×
195
                exceptionMessage.append("\nAdditional info: ").append(warning);
×
196
            }
197
            exceptionMessage.append("\n - Class: ").append(initContext.displayClass());
×
198
            exceptionMessage.append("\n - Method: ").append(initContext.displayMethod());
×
199
            throw new IllegalStateException(exceptionMessage.toString());
×
200
        }
201
        return parameterValues[parameterIndex];
1✔
202
    }
203

204
    @Override
205
    public void handleTestExecutionException(@NonNull ExtensionContext context, @NonNull Throwable throwable)
206
            throws Throwable {
207
        if (isExpectedException(context, throwable)) {
1!
208
            // Expected exception was thrown, suppress it (test passes)
209
            return;
1✔
210
        }
211

212
        thrownByTest = throwable;
×
213
        throw throwable;
×
214
    }
215

216
    @Override
217
    public void afterTestExecution(@NonNull ExtensionContext context) {
218
        if (savePointForTestMethod == null) {
1!
219
            return;
×
220
        }
221

222
        TestRun.enterNoMockingZone();
1✔
223

224
        try {
225
            savePointForTestMethod.rollback();
1✔
226
            savePointForTestMethod = null;
1✔
227

228
            if (thrownByTest != null) {
1!
NEW
229
                StackTrace.filterStackTrace(thrownByTest);
×
230
            }
231

232
            Error expectationsFailure = RecordAndReplayExecution.endCurrentReplayIfAny();
1✔
233
            clearTestedObjectsIfAny();
1✔
234

235
            if (expectationsFailure != null && isExpectedException(context, expectationsFailure)) {
1!
236
                // Expected JMockit error was thrown, suppress it (test passes)
237
                return;
1✔
238
            }
239

240
            if (expectationsFailure != null) {
1!
NEW
241
                StackTrace.filterStackTrace(expectationsFailure);
×
242
                throw expectationsFailure;
×
243
            }
244
        } finally {
245
            TestRun.finishCurrentTestExecution();
1✔
246
            TestRun.exitNoMockingZone();
1✔
247
        }
248
    }
1✔
249

250
    @Override
251
    public void afterEach(@NonNull ExtensionContext context) {
252
        if (savePointForTest != null) {
1!
253
            savePointForTest.rollback();
1✔
254
            savePointForTest = null;
1✔
255
        }
256
    }
1✔
257

258
    @Override
259
    public void afterAll(@NonNull ExtensionContext context) {
260
        if (savePointForTestClass != null && isRegularTestClass(context)) {
1!
261
            savePointForTestClass.rollback();
1✔
262
            savePointForTestClass = null;
1✔
263

264
            clearFieldTypeRedefinitions();
1✔
265
            TestRun.setCurrentTestClass(null);
1✔
266
        }
267
    }
1✔
268

269
    private static class ParamValueInitContext {
270
        private final Object instance;
271
        private final Class<?> clazz;
272
        private final Method method;
273
        private final String warning;
274

275
        ParamValueInitContext(Object instance, Class<?> clazz, Method method, String warning) {
1✔
276
            this.instance = instance;
1✔
277
            this.clazz = clazz;
1✔
278
            this.method = method;
1✔
279
            this.warning = warning;
1✔
280
        }
1✔
281

282
        boolean isBeforeAllMethod() {
283
            return method != null && method.getDeclaredAnnotation(BeforeAll.class) != null;
×
284
        }
285

286
        boolean isBeforeEachMethod() {
287
            return method != null && method.getDeclaredAnnotation(BeforeEach.class) != null;
×
288
        }
289

290
        String displayClass() {
291
            return clazz == null ? "<no class reference>" : clazz.getName();
×
292
        }
293

294
        String displayMethod() {
295
            if (method == null) {
×
296
                return "<no method reference>";
×
297
            }
298
            String methodPrefix = isBeforeAllMethod() ? "@BeforeAll " : isBeforeEachMethod() ? "@BeforeEach " : "";
×
299
            String args = Arrays.stream(method.getParameterTypes()).map(Class::getName)
×
300
                    .collect(Collectors.joining(", "));
×
301
            return methodPrefix + method.getName() + "(" + args + ")";
×
302
        }
303

304
        @Override
305
        public String toString() {
306
            return "ParamContext{hasInstance=" + (instance == null ? "false" : "true") + ", class=" + clazz
×
307
                    + ", method=" + method + ", warning=" + warning + "}";
308
        }
309
    }
310

311
    private static boolean isExpectedException(@NonNull ExtensionContext context, @NonNull Throwable throwable) {
312
        Method testMethod = context.getTestMethod().orElse(null);
1✔
313
        ExpectedException expectedException = testMethod != null ? testMethod.getAnnotation(ExpectedException.class)
1!
314
                : null;
1✔
315

316
        if (expectedException == null) {
1!
NEW
317
            return false;
×
318
        }
319

320
        return expectedException.value().isInstance(throwable) && matchesExpectedMessages(throwable, expectedException);
1!
321
    }
322

323
    private static boolean matchesExpectedMessages(Throwable throwable, ExpectedException expectedException) {
324
        String[] expectedMessages = expectedException.expectedMessages();
1✔
325
        if (expectedMessages.length == 0) {
1✔
326
            // No message requirement
327
            return true;
1✔
328
        }
329

330
        String actualMessage = throwable.getMessage();
1✔
331
        if (actualMessage == null) {
1!
NEW
332
            return false;
×
333
        }
334

335
        boolean contains = expectedException.messageContains();
1✔
336
        for (String expected : expectedMessages) {
1!
337
            if (contains) {
1!
338
                if (actualMessage.contains(expected)) {
1!
339
                    return true;
1✔
340
                }
NEW
341
            } else if (actualMessage.equals(expected)) {
×
NEW
342
                return true;
×
343
            }
344
        }
NEW
345
        return false;
×
346
    }
347

348
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc