0
# Main Method Testing
1
2
Quarkus JUnit 5 provides comprehensive support for testing command-line applications that use Quarkus main method execution.
3
4
## Main Method Test Annotations
5
6
### @QuarkusMainTest
7
8
Tests the main method within the same JVM as the test, creating a new in-memory Quarkus application that runs to completion.
9
10
```java { .api }
11
@Target(ElementType.TYPE)
12
@Retention(RetentionPolicy.RUNTIME)
13
@ExtendWith(QuarkusMainTestExtension.class)
14
public @interface QuarkusMainTest {
15
}
16
```
17
18
### @QuarkusMainIntegrationTest
19
20
Tests the main method against built artifacts (JAR, native image, or container), running in a separate process.
21
22
```java { .api }
23
@Target(ElementType.TYPE)
24
@Retention(RetentionPolicy.RUNTIME)
25
@ExtendWith(QuarkusMainIntegrationTestExtension.class)
26
public @interface QuarkusMainIntegrationTest {
27
}
28
```
29
30
## Launch Annotations and Interfaces
31
32
### @Launch Annotation
33
34
Annotation for launching command-line applications with specified arguments and expected exit codes.
35
36
```java { .api }
37
@Target(ElementType.METHOD)
38
@Retention(RetentionPolicy.RUNTIME)
39
public @interface Launch {
40
/**
41
* The program arguments to launch with
42
*/
43
String[] value() default "";
44
45
/**
46
* Expected return code
47
*/
48
int exitCode() default 0;
49
}
50
```
51
52
### QuarkusMainLauncher Interface
53
54
Interface for programmatically launching command-line applications with arbitrary parameters.
55
56
```java { .api }
57
public interface QuarkusMainLauncher {
58
/**
59
* Launch the command line application with the given parameters.
60
*/
61
LaunchResult launch(String... args);
62
}
63
```
64
65
### LaunchResult Interface
66
67
Contains information about a command-line application execution run.
68
69
```java { .api }
70
public interface LaunchResult {
71
/**
72
* Get the command line application standard output as a single string.
73
*/
74
default String getOutput() {
75
return String.join("\n", getOutputStream());
76
}
77
78
/**
79
* Get the command line application error output as a single string.
80
*/
81
default String getErrorOutput() {
82
return String.join("\n", getErrorStream());
83
}
84
85
/**
86
* Echo the command line application standard output to the console.
87
*/
88
default void echoSystemOut() {
89
System.out.println(getOutput());
90
System.out.println();
91
}
92
93
/**
94
* Get the command line application standard output as a list of strings.
95
* Each line of output correspond to a string in the list.
96
*/
97
List<String> getOutputStream();
98
99
/**
100
* Get the command line application error output as a list of strings.
101
* Each line of output correspond to a string in the list.
102
*/
103
List<String> getErrorStream();
104
105
/**
106
* Get the exit code of the application.
107
*/
108
int exitCode();
109
}
110
```
111
112
## Basic Usage Examples
113
114
### Simple Main Method Test
115
116
```java
117
import io.quarkus.test.junit.main.QuarkusMainTest;
118
import io.quarkus.test.junit.main.Launch;
119
import io.quarkus.test.junit.main.LaunchResult;
120
import org.junit.jupiter.api.Test;
121
import static org.junit.jupiter.api.Assertions.*;
122
123
@QuarkusMainTest
124
class CalculatorMainTest {
125
126
@Test
127
@Launch({"add", "5", "3"})
128
void testAddCommand(LaunchResult result) {
129
assertEquals(0, result.exitCode());
130
assertTrue(result.getOutput().contains("Result: 8"));
131
}
132
133
@Test
134
@Launch(value = {"divide", "10", "0"}, exitCode = 1)
135
void testDivisionByZero(LaunchResult result) {
136
assertEquals(1, result.exitCode());
137
assertTrue(result.getErrorOutput().contains("Division by zero"));
138
}
139
140
@Test
141
@Launch({"--help"})
142
void testHelpOutput(LaunchResult result) {
143
assertEquals(0, result.exitCode());
144
String output = result.getOutput();
145
assertTrue(output.contains("Usage:"));
146
assertTrue(output.contains("Commands:"));
147
}
148
}
149
```
150
151
### Programmatic Launch Testing
152
153
```java
154
import io.quarkus.test.junit.main.QuarkusMainTest;
155
import io.quarkus.test.junit.main.QuarkusMainLauncher;
156
import io.quarkus.test.junit.main.LaunchResult;
157
import org.junit.jupiter.api.Test;
158
159
@QuarkusMainTest
160
class FileProcessorMainTest {
161
162
@Test
163
void testFileProcessing(QuarkusMainLauncher launcher) {
164
// Test successful file processing
165
LaunchResult result = launcher.launch("process", "/tmp/input.txt", "/tmp/output.txt");
166
assertEquals(0, result.exitCode());
167
assertTrue(result.getOutput().contains("Processing completed"));
168
169
// Test missing file
170
LaunchResult errorResult = launcher.launch("process", "/nonexistent/file.txt");
171
assertEquals(2, errorResult.exitCode());
172
assertTrue(errorResult.getErrorOutput().contains("File not found"));
173
}
174
175
@Test
176
void testValidationMode(QuarkusMainLauncher launcher) {
177
LaunchResult result = launcher.launch("validate", "--strict", "/tmp/data.xml");
178
179
if (result.exitCode() == 0) {
180
assertTrue(result.getOutput().contains("Validation passed"));
181
} else {
182
assertTrue(result.getErrorOutput().contains("Validation failed"));
183
}
184
}
185
}
186
```
187
188
### Integration Testing
189
190
```java
191
import io.quarkus.test.junit.main.QuarkusMainIntegrationTest;
192
import io.quarkus.test.junit.main.Launch;
193
import io.quarkus.test.junit.main.LaunchResult;
194
import org.junit.jupiter.api.Test;
195
196
@QuarkusMainIntegrationTest
197
class DatabaseMigratorIT {
198
199
@Test
200
@Launch({"migrate", "--url", "jdbc:h2:mem:test"})
201
void testDatabaseMigration(LaunchResult result) {
202
assertEquals(0, result.exitCode());
203
204
List<String> outputLines = result.getOutputStream();
205
assertTrue(outputLines.stream().anyMatch(line ->
206
line.contains("Migration completed successfully")));
207
}
208
209
@Test
210
@Launch(value = {"rollback", "--version", "invalid"}, exitCode = 1)
211
void testInvalidRollback(LaunchResult result) {
212
assertEquals(1, result.exitCode());
213
assertTrue(result.getErrorOutput().contains("Invalid version"));
214
}
215
}
216
```
217
218
## Advanced Testing Patterns
219
220
### Configuration-Dependent Testing
221
222
```java
223
@QuarkusMainTest
224
class ConfigurableAppTest {
225
226
@Test
227
void testWithDifferentConfigs(QuarkusMainLauncher launcher) {
228
// Test with development config
229
LaunchResult devResult = launcher.launch("--profile", "dev", "start");
230
assertEquals(0, devResult.exitCode());
231
232
// Test with production config
233
LaunchResult prodResult = launcher.launch("--profile", "prod", "start");
234
assertEquals(0, prodResult.exitCode());
235
236
// Verify different behaviors
237
assertNotEquals(devResult.getOutput(), prodResult.getOutput());
238
}
239
}
240
```
241
242
### Multi-Command Application Testing
243
244
```java
245
@QuarkusMainTest
246
class CliToolTest {
247
248
@Test
249
void testAllCommands(QuarkusMainLauncher launcher) {
250
// Test create command
251
LaunchResult createResult = launcher.launch("create", "project", "my-app");
252
assertEquals(0, createResult.exitCode());
253
assertTrue(createResult.getOutput().contains("Project created"));
254
255
// Test list command
256
LaunchResult listResult = launcher.launch("list", "projects");
257
assertEquals(0, listResult.exitCode());
258
assertTrue(listResult.getOutput().contains("my-app"));
259
260
// Test delete command
261
LaunchResult deleteResult = launcher.launch("delete", "project", "my-app");
262
assertEquals(0, deleteResult.exitCode());
263
assertTrue(deleteResult.getOutput().contains("Project deleted"));
264
}
265
}
266
```
267
268
### Output Parsing and Validation
269
270
```java
271
@QuarkusMainTest
272
class DataAnalyzerTest {
273
274
@Test
275
@Launch({"analyze", "--format", "json", "/tmp/data.csv"})
276
void testJsonOutput(LaunchResult result) {
277
assertEquals(0, result.exitCode());
278
279
String jsonOutput = result.getOutput();
280
281
// Parse and validate JSON structure
282
ObjectMapper mapper = new ObjectMapper();
283
JsonNode json = mapper.readTree(jsonOutput);
284
285
assertTrue(json.has("summary"));
286
assertTrue(json.has("results"));
287
assertTrue(json.get("results").isArray());
288
}
289
290
@Test
291
@Launch({"analyze", "--format", "csv", "/tmp/data.csv"})
292
void testCsvOutput(LaunchResult result) {
293
assertEquals(0, result.exitCode());
294
295
List<String> lines = result.getOutputStream();
296
297
// Validate CSV format
298
assertTrue(lines.get(0).contains("column1,column2,column3")); // Header
299
assertTrue(lines.size() > 1); // Has data rows
300
301
// Validate each data row
302
lines.stream().skip(1).forEach(line -> {
303
String[] columns = line.split(",");
304
assertEquals(3, columns.length);
305
});
306
}
307
}
308
```
309
310
### Error Handling and Exit Codes
311
312
```java
313
@QuarkusMainTest
314
class ErrorHandlingTest {
315
316
@Test
317
void testErrorConditions(QuarkusMainLauncher launcher) {
318
// Test invalid command
319
LaunchResult invalidCmd = launcher.launch("invalid-command");
320
assertEquals(1, invalidCmd.exitCode());
321
assertTrue(invalidCmd.getErrorOutput().contains("Unknown command"));
322
323
// Test missing required argument
324
LaunchResult missingArg = launcher.launch("process");
325
assertEquals(2, missingArg.exitCode());
326
assertTrue(missingArg.getErrorOutput().contains("Missing required"));
327
328
// Test invalid argument value
329
LaunchResult invalidArg = launcher.launch("process", "--threads", "invalid");
330
assertEquals(3, invalidArg.exitCode());
331
assertTrue(invalidArg.getErrorOutput().contains("Invalid number"));
332
}
333
}
334
```
335
336
## Testing Best Practices
337
338
### Test Organization
339
340
```java
341
// Base test class for shared test logic
342
@QuarkusMainTest
343
class BaseMainTest {
344
345
protected void assertSuccessfulExecution(LaunchResult result) {
346
assertEquals(0, result.exitCode());
347
assertFalse(result.getOutput().trim().isEmpty());
348
}
349
350
protected void assertErrorExecution(LaunchResult result, String expectedError) {
351
assertNotEquals(0, result.exitCode());
352
assertTrue(result.getErrorOutput().contains(expectedError));
353
}
354
}
355
356
// Integration test extends base test
357
@QuarkusMainIntegrationTest
358
class MainIntegrationTest extends BaseMainTest {
359
// Same test methods run against built artifact
360
}
361
```
362
363
### Resource Management
364
365
```java
366
@QuarkusMainTest
367
class ResourceManagedTest {
368
369
private Path tempDir;
370
371
@BeforeEach
372
void setupTempDirectory() throws IOException {
373
tempDir = Files.createTempDirectory("test");
374
}
375
376
@AfterEach
377
void cleanupTempDirectory() throws IOException {
378
Files.walk(tempDir)
379
.sorted(Comparator.reverseOrder())
380
.map(Path::toFile)
381
.forEach(File::delete);
382
}
383
384
@Test
385
void testFileOperation(QuarkusMainLauncher launcher) {
386
Path inputFile = tempDir.resolve("input.txt");
387
Files.write(inputFile, "test data".getBytes());
388
389
LaunchResult result = launcher.launch("process", inputFile.toString());
390
assertEquals(0, result.exitCode());
391
}
392
}
393
```
394
395
### Performance Testing
396
397
```java
398
@QuarkusMainTest
399
class PerformanceTest {
400
401
@Test
402
@Timeout(value = 30, unit = TimeUnit.SECONDS)
403
void testPerformanceRequirement(QuarkusMainLauncher launcher) {
404
long startTime = System.currentTimeMillis();
405
406
LaunchResult result = launcher.launch("heavy-operation", "--size", "1000");
407
assertEquals(0, result.exitCode());
408
409
long duration = System.currentTimeMillis() - startTime;
410
assertTrue(duration < 30000, "Operation took too long: " + duration + "ms");
411
}
412
}
413
```
414
415
## Limitations and Considerations
416
417
### CDI Injection Limitation
418
```java
419
@QuarkusMainTest
420
class MainTestLimitations {
421
422
// ❌ Not supported in main method tests
423
// @Inject
424
// SomeService service;
425
426
@Test
427
void testWithoutInjection(QuarkusMainLauncher launcher) {
428
// Must test through main method execution only
429
LaunchResult result = launcher.launch("command");
430
assertEquals(0, result.exitCode());
431
}
432
}
433
```
434
435
### Application Lifecycle
436
- Each test method starts a new application instance
437
- Application runs to completion before test method continues
438
- Cannot test long-running applications directly (use integration tests instead)
439
440
### Native Image Testing
441
```java
442
// Works with both JVM and native image builds
443
@QuarkusMainIntegrationTest
444
class NativeCompatibleTest {
445
446
@Test
447
@Launch({"quick-command"})
448
void testNativeCompatibility(LaunchResult result) {
449
// Same test works for both JVM and native image
450
assertEquals(0, result.exitCode());
451
}
452
}
453
```