System Lambda is a collection of functions for testing code which uses
java.lang.System.
System Lambda is published under the MIT license. It requires at least Java 8.
For JUnit 4 there is an alternative to System Lambda. Its name is System Rules.
System Lambda is available from Maven Central.
<dependency>
<groupId>com.github.stefanbirkner</groupId>
<artifactId>system-lambda</artifactId>
<version>1.2.1</version>
</dependency>Please don't forget to add the scope test if you're using System Lambda for
tests only. The changelog lists all the changes of System
Lambda.
Import System Lambda's functions by adding
importstaticcom.github.stefanbirkner.systemlambda.SystemLambda.*;to your tests.
Command-line applications terminate by calling System.exit with some status
code. If you test such an application then the JVM that executes the test exits
when the application under test calls System.exit. You can avoid this with
the method catchSystemExit which also returns the status code of the
System.exit call.
@Testvoidapplication_exits_with_status_42(
) throwsException {
intstatusCode = catchSystemExit(() -> {
System.exit(42);
});
assertEquals(42, statusCode);
}The method catchSystemExit throws an AssertionError if the code under test
does not call System.exit. Therefore your test fails with the failure message
"System.exit has not been called."
The method withEnvironmentVariable allows you to set environment variables
within your test code that are removed after your code under test is executed.
@Testvoidexecute_code_with_environment_variables(
) throwsException {
List<String> values = withEnvironmentVariable("first", "first value")
.and("second", "second value")
.execute(() -> asList(
System.getenv("first"),
System.getenv("second")
));
assertEquals(
asList("first value", "second value"),
values
);
}The method restoreSystemProperties guarantees that after executing the test
code each System property has the same value like before. Therefore you can
modify System properties inside of the test code without having an impact on
other tests.
@Testvoidexecute_code_that_manipulates_system_properties(
) throwsException {
restoreSystemProperties(() -> {
System.setProperty("some.property", "some value");
//code under test that reads properties (e.g. "some.property") or//modifies them.
});
//Here the value of "some.property" is the same like before.//E.g. it is not set.
}Command-line applications usually write to the console. If you write such
applications you need to test the output of these applications. The methods
tapSystemErr, tapSystemErrNormalized, tapSystemOut,
tapSystemOutNormalized, tapSystemErrAndOut and
tapSystemErrAndOutNormalized allow you to tap the text that is written to
System.err/System.out. The methods with the suffix Normalized normalize
line breaks to \n so that you can run tests with the same assertions on
different operating systems.
@Testvoidapplication_writes_text_to_System_err(
) throwsException {
Stringtext = tapSystemErr(() -> {
System.err.print("some text");
});
assertEquals("some text", text);
}
@Testvoidapplication_writes_mutliple_lines_to_System_err(
) throwsException {
Stringtext = tapSystemErrNormalized(() -> {
System.err.println("first line");
System.err.println("second line");
});
assertEquals("first line\nsecond line\n", text);
}
@Testvoidapplication_writes_text_to_System_out(
) throwsException {
Stringtext = tapSystemOut(() -> {
System.out.print("some text");
});
assertEquals("some text", text);
}
@Testvoidapplication_writes_mutliple_lines_to_System_out(
) throwsException {
Stringtext = tapSystemOutNormalized(() -> {
System.out.println("first line");
System.out.println("second line");
});
assertEquals("first line\nsecond line\n", text);
}
@Testvoidapplication_writes_text_to_System_err_and_out(
) throwsException {
Stringtext = tapSystemErrAndOut(() -> {
System.err.print("text from err");
System.out.print("text from out");
});
assertEquals("text from errtext from out", text);
}
@Testvoidapplication_writes_mutliple_lines_to_System_err_and_out(
) throwsException {
Stringtext = tapSystemErrAndOutNormalized(() -> {
System.err.println("text from err");
System.out.println("text from out");
});
assertEquals("text from err\ntext from out\n", text);
}You can assert that nothing is written to System.err/System.out by wrapping
code with the function
assertNothingWrittenToSystemErr/assertNothingWrittenToSystemOut. E.g. the
following tests fail:
@Testvoidfails_because_something_is_written_to_System_err(
) throwsException {
assertNothingWrittenToSystemErr(() -> {
System.err.println("some text");
});
}
@Testvoidfails_because_something_is_written_to_System_out(
) throwsException {
assertNothingWrittenToSystemOut(() -> {
System.out.println("some text");
});
}If the code under test writes text to System.err/System.out then it is
intermixed with the output of your build tool. Therefore you may want to avoid
that the code under test writes to System.err/System.out. You can achieve
this with the function muteSystemErr/muteSystemOut. E.g. the following tests
don't write anything to System.err/System.out:
@Testvoidnothing_is_written_to_System_err(
) throwsException {
muteSystemErr(() -> {
System.err.println("some text");
});
}
@Testvoidnothing_is_written_to_System_out(
) throwsException {
muteSystemOut(() -> {
System.out.println("some text");
});
}Interactive command-line applications read from System.in. If you write such
applications you need to provide input to these applications. You can specify
the lines that are available from System.in with the method
withTextFromSystemIn
@TestvoidScanner_reads_text_from_System_in(
) throwsException {
withTextFromSystemIn("first line", "second line")
.execute(() -> {
Scannerscanner = newScanner(System.in);
scanner.nextLine();
assertEquals("second line", scanner.nextLine());
});
}For a complete test coverage you may also want to simulate System.in throwing
exceptions when the application reads from it. You can specify such an
exception (either RuntimeException or IOException) after specifying the
text. The exception will be thrown by the next read after the text has been
consumed.
@TestvoidSystem_in_throws_IOException(
) throwsException {
withTextFromSystemIn("first line", "second line")
.andExceptionThrownOnInputEnd(newIOException())
.execute(() -> {
Scannerscanner = newScanner(System.in);
scanner.nextLine();
scanner.nextLine();
assertThrows(
IOException.class,
() -> scanner.readLine()
);
});
}
@TestvoidSystem_in_throws_RuntimeException(
) throwsException {
withTextFromSystemIn("first line", "second line")
.andExceptionThrownOnInputEnd(newRuntimeException())
.execute(() -> {
Scannerscanner = newScanner(System.in);
scanner.nextLine();
scanner.nextLine();
assertThrows(
RuntimeException.class,
() -> scanner.readLine()
);
});
}You can write a test that throws an exception immediately by not providing any text.
withTextFromSystemIn()
.andExceptionThrownOnInputEnd(...)
.execute(() -> {
Scannerscanner = newScanner(System.in);
assertThrows(
...,
() -> scanner.readLine()
);
});The function withSecurityManager lets you specify the SecurityManager that
is returned by System.getSecurityManger() while your code under test is
executed.
@Testvoidexecute_code_with_specific_SecurityManager(
) throwsException {
SecurityManagersecurityManager = newASecurityManager();
withSecurityManager(
securityManager,
() -> {
//code under test//e.g. the following assertion is metassertSame(
securityManager,
System.getSecurityManager()
);
}
);
}After withSecurityManager(...) is executedSystem.getSecurityManager()
returns the original security manager again.
You have three options if you have a feature request, found a bug or simply have a question about System Lambda.
- Write an issue.
- Create a pull request. (See Understanding the GitHub Flow)
- Write a mail to mail@stefan-birkner.de
System Lambda is built with Maven and must be compiled under JDK 8. If you want to contribute code then
- Please write a test for your change.
- Ensure that you didn't break the build by running
mvnw clean verify -Dgpg.skip. - Fork the repo and create a pull request. (See Understanding the GitHub Flow)
The basic coding style is described in the
EditorConfig file .editorconfig.
System Lambda supports GitHub Actions (Linux) and AppVeyor (Windows) for continuous integration. Each pull request is automatically built by both CI servers. GitHub Actions tests with several Java versions (see Enhanced Testing) while AppVeyor tests with Java 8 only.
There are decision records for some decisions that had been made for System Lambda. They are stored in the folder doc/Decision Records
The script
./scripts/mvn_in_docker.sh clean verify -Dgpg.skip
builds System Lambda inside a Docker container. It uses your Maven local repository. This is helpful if you have a Linux or macOS without JDK 8.
System Lambda is built with Java 8 and relies on JDK internals that are not available from Java 16 on (unless you run Java with additional flags). We verify that System Lambda works with newer Java versions by building it with OpenJDK 8 and executing tests with newer versions of OpenJDK. All this work is put into a script that you can run with
./scripts/test.sh
The script uses Docker for running the tests.
- Select a new version according to the Semantic Versioning 2.0.0 Standard.
- Update
Changelog.md. - Set the new version in
pom.xmland in theInstallationsection of this readme. - Commit the modified
Changelog.md,pom.xmlandREADME.md. - Run
mvnw clean deploywith JDK 8. - Add a tag for the release:
git tag system-lambda-X.X.X