-
Notifications
You must be signed in to change notification settings - Fork 0
FAT tests
- Overview of Functional Acceptance Tests (FAT)
- FAT Project Layout
- How to run an existing FAT
- Overview of how a FAT test runs
- Using the fattest.simplicity infrastructure
- Debugging a FAT
- FAT test modes
- Skipping tests based on java level
- Error/Warning/FFDC scanning
- Java 2 Security testing
- Repeating FAT tests for different feature/JEE levels
The majority of OpenLiberty tests are Functional Acceptance Tests (FAT). A FAT test project ends in _fat
and executes tests with a running OpenLiberty image. This differs from Unit tests because Unit tests do not execute on a running OpenLiberty image.
The example FAT project is build.example_fat
and can be used as a minimalistic reference point for writing new FATs.
Note that TCK projects follow a slightly different layout. A TCK project ends in _fat_tck
and require mvn
to be installed when run locally. See here for more information on creating and running a TCK project.
- com.ibm.ws.my_fat/
- fat/src/ # Root folder for test client side
- com/ibm/ws/my/
- FATSuite.java # Entry point of the FAT. Lists one or more test classes via '@SuiteClasses'
- MyTest.java # Test class containing a group of tests. Server and app lifecycle is controlled here.
- test-applications/ # Root folder for all test applications
- myApp/ # Folder name indicates name of application (by convention)
- resources/ # Optional: Static resources such as web.xml are included here for 'myApp'
- src/ # Java code for 'myApp' goes here
- publish/ # Root folder for non-java resources
- files/ # Files needed by test client go here. Such as alternate server.xml configurations
- servers/ # Root folder for OpenLiberty servers
- myServer/ # Folder representing a server. This folder gets copied to ${wlp.install.dir} at runtime
- server.xml # Server configuration for 'myServer'
- build/libs/autoFVT/ # Root folder for a single execution of FAT output
- output/ # Root folder for server output
- results/ # Root folder for test client output
- junit.html/ # Contains JUnit-style test results which can be viewed in a web browser
- output.txt # Log file for test client (i.e. fat/src/ classes)
- bnd.bnd # Describes source folders and compile-time build dependencies
- build.gradle # Build file. Must include "apply from: '../cnf/gradle/scripts/fat.gradle'"
To run a FAT, you must have ant
on your $PATH
.
To test this, you can run the command which ant
. If ant is not on your path, you can download ant here and then add export PATH=$ANT_HOME/bin:$PATH
to your ~/.bashrc. After adding ant to your path, run ./gradlew --stop
to stop the Gradle daemon so that it can be started with the updated $PATH
.
./gradlew <fat_project_name>:buildandrun
For example:
./gradlew build.example_fat:buildandrun
The buildandrun
task will build and run the FAT. If you are not making changes to the FAT and just want to re-run the FAT without rebuilding it, you can use the runfat
task instead.
./gradlew <fat_project_name>:buildandrun -Dfat.test.class.name=<fully_qualified_java_test_class>
For example:
./gradlew build.example_fat:buildandrun -Dfat.test.class.name=com.ibm.ws.example.SimpleTest
When a FAT runs there are two or more JVMs in use:
- A test-client side JVM which manages server lifecycle, deploys apps, and drives individual test cases on the server side
- An OpenLiberty server with zero or more test apps running on it.
Most FATs use a test client to drive HTTP GET requests on a test servlet within a test application. The steps for this style of FAT test is the following.
- Build packages everything needed to run the FAT into an autoFVT.zip archive
- On an automated build, the autoFVT.zip for each FAT is built on the parent build machine. Then the autoFVT.zip archives are distributed evenly among about a dozen child build engines where they will be extracted and run. This allows us to pack about 36 hours of FAT tests into about 3 hours.
- Build unzips autoFVT.zip and starts a JUnit process with the class named
FATSuite
as the entry point. Each test class in the@SuiteClasses
will be run in sequence. - Test class runs
- Test class
@BeforeClass
runs. Typically applications are deployed to server and then server is started here. - Test client runs each test case in class in sequence. Typically each test case is a single HTTP GET. For example
GET http://localhost:8010/myApp/MyTestServlet?testMethod=myTest
- Test application receives HTTP GET request via a Servlet. For example, inside
MyTestServlet.doGet()
thetestMethod
parameter is checked, and reflection is used to invoke a method indicated by the parameter value, soMyTestSevlet.myTest()
is invoked. - If the test case passes, the test servlet prints a well-known success message string to the HTTPServletResponse. If the test case fails, the test servlet does not print the success message, and instead dumps the stack trace of the failure to the HTTPServletResponse.
- Test client reads HTTPServletResponse. If response contains well-known success message, the test passes. If the success message is missing, fail the test case with the HTTPServletResponse content as the failure message.
- Test application receives HTTP GET request via a Servlet. For example, inside
- Test class
@AfterClass
runs. Typically server is stopped here. When server is stopped, the server logs are copied from${wlp.install.dir}
intobuild/libs/autoFVT/output/servers/<server_name>-<stop_timestamp>
- Test class
The FAT test infrastructure is housed in the fattest.simplicity
project. This contains all of the junit extensions and extra bells and whistles available to FAT projects.
To get most of the custom value-adds of the simplicity infrastructure, test classes must indicate @RunWith(FATRunner.class)
on the test class. For example:
@RunWith(FATRunner.class)
public class SimpleTest { /* ... */ }
The LibertyServer
class gives the test client a POJO representation of an OpenLiberty server. The easiest way to obtain an object instance is with the @Server("<server_name>")
annotation. With this object, you can do things like start and stop a server.
An example usage typically looks like:
@RunWith(FATRunner.class)
public class MyFATTest {
@Server("MyServer")
public static LibertyServer server;
@BeforeClass
public static void setup() throws Exception {
server.startServer();
}
@AfterClass
public static void tearDown() throws Exception {
server.stopServer();
}
}
Typically FATs build application artifacts at FAT runtime using an open source library called ShrinkWrap, and then export the artifacts to disk. The ShrinkWrap API is quite flexible and can build many different types of archives including: .zip .jar .war .ear .rar
The official ShrinkWrap javadoc can be found here: ShrinkWrap API 1.2.3 API
The ShrinkWrap API can get a little verbose, so we have a ShrinkHelper
class to make conventional operations more concise. A few useful ones are:
-
ShrinkHelper.defaultApp(LibertyServer s, String appName, String... packages)
Builds an artifact named<appName>.war
which includes resources fromtest-applications/<appName>/resources/
and the specified java packages. Also exports the .war to the indicated LibertyServer's${server.config.dir}/apps/
directory. -
ShrinkHelper.defaultDropinApp(LibertyServer s, String appName, String... packages)
The same thing asShrinkHelper#defaultApp
except it exports the .war to the${server.config.dir}/dropins/
, which means no server.xml configuration is needed for the application. -
ShrinkHelper.buildDefaultApp(String appName, String... packages)
The same thing asShrinkHelper#defaultApp
except the archive is not exported to disk anywhere. -
ShrinkHelper.exportAppToServer(LibertyServer s, Archive<?> a)
Exports the archive to disk in the${server.config.dir}/apps/
directory. The name of the exported file will bea.getName()
. -
ShrinkHelper.exportToServer(LibertyServer s, String path, Archive<?> a)
Same thing asShrinkHelper#exportAppToServer
except you can indicate apath
to export the archive at (relative to${server.config.dir}
of the indicated server). -
ShrinkHelper.addDirectory(Archive<?> a, String path)
Adds a directory (and all sub-dirs) to the indicated archive.
There is a test-only feature in Liberty called componentTest-1.0
which will provide some common utilities for FAT test apps.
- If the
componentTest-1.0
feature is enabled in server.xml, then JUnit will be made available to test applications. This is useful for being able to do server-side assertions. - If the
componentTest-1.0
feature is enabled in server.xml, theFATServlet
class is made available to test applications. This class contains boiler-plate HttpServelt code for invoking test methods.
Declaring test methods on the server side using @TestServlet
Without @TestServlet
With standard JUnit a "test stub" would be needed on the client side for each server-side test. For example:
// Client side
public class MyTest {
// get and start server in @BeforeClass
@Test
public void myTest() throws Exception {
// invoke GET http://loclahost:8010/myApp/MyTestServlet?testMethod=myTest and assert response is successful
FATServletClient.runTest(server, "/myApp/MyTestServlet", "myTest");
}
}
// Server side
@WebServlet("/MyTestServlet")
public class MyTestServlet extends FATServlet {
public void myTest() throws Exception {
// run some server-side test
}
}
This is inconvenient because the client side code is completely boiler plate and if the server side test moves or is renamed it requires a corresponding update on the client side class.
With @TestServlet
To eliminate this, we have the @TestServlet
annotation which will scan the referenced test servlet class for @Test
methods and add synthetic client-side test stubs at runtime. So the example above becomes:
// Client side
@RunWith(FATRunner.class)
public class MyTest {
@Server("MyServer")
@TestServlet(servlet = MyTestServlet.class, contextRoot = "myApp") // References class gets scanned for @Test methods
public static LibertyServer server;
// start server in @BeforeClass
}
// Server side
@WebServlet("/MyTestServlet")
public class MyTestServlet extends FATServlet {
@Test // Able to use standard JUnit @Test annotation in test servlet
public void myTest() throws Exception {
// run some server-side test
}
}
In a typical FAT there are two or more JVMs (client driver, and a server).
To debug the test client, use the -Ddebug.framework
option and attach a debugger on port 6666. For example:
./gradlew build.example_fat:buildandrun -Ddebug.framework
If you are using the Eclipse IDE, you can attach a debugger by selecting the debug configuration Liberty-FAT remote debug
from the debug menu dropdown. If you've never used this debug configuration, it will not appear in the dropdown until you've visited Debug Configurations... from the Debug dropdown. Look for 'Liberty-FAT remote debug' under 'Remote Java Application'.
To debug a liberty server, use the -Ddebug.server
option and attach a debugger on port 7777. For example:
./gradlew build.example_fat:buildandrun -Ddebug.server
If you are using the Eclipse IDE, you can attach the debugger by selecting Liberty- server remote debug
from the debug menu dropdown. See above if this item doesn't appear in your debug dropdown.
A FAT project should run in 5 minutes or less, with each test case running in under 15 seconds. However, sometimes it is necessary to write tests that are important to run, but also time intensive.
The simplicity test framework has an @Mode
annotation for this purpose. There are two primary modes: LITE
and FULL
, with LITE
being the default mode. If a test method does not define a @Mode
annotation, it will be considered a LITE
mode test. For example:
public class MyTest {
@Test
public void regularTest() { } // No @Mode annotation, so LITE mode is assumed
@Test
@Mode(LITE)
public void liteTest() { } // functionally equivalent to 'regularTest()'
@Test
@Mode(FULL)
public void testThatTakesAWhile() { } // Will only be run the FAT is run in FULL mode
}
To run a FAT in FULL
mode, specify -Dfat.test.mode=FULL
in the launch command, for example:
./gradlew build.example_fat:buildandrun -Dfat.test.mode=FULL
Currently OpenLiberty supports Java 7 and 8, with all new features requiring a minimum of Java 8 to run. As a result, any FATs for new features must not run on Java 7.
There are 3 ways to skip FAT tests based on Java level:
- Setting
javac.source: 1.8
in the bnd.bnd file (i.e. compiling the FAT with Java 8) will indicate that the entire FAT should skip on Java 7. - Setting
runtime.minimum.java.level: 1.8
in the bnd.bnd file will indicate that the entire FAT should skip on Java 7. - Using the
@MinimumJavaLevel(javaLevel = 1.8)
annotation on a test class or method will cause the annotated element to skip on Java 7.
Sometimes things can go wrong during a FAT test case that doesn't cause the JUnit test case to fail, such as an error or warning message in server logs. To catch this, whenever a LibertyServer is stopped, the messages.log file will be scanned for errors and warnings. If any errors or warnings are found, an exception is thrown which will trigger a class-level JUnit failure.
If a test generates errors or warnings that are expected to occur, then a list of expected errors can be passed into LibertyServer#stopServer. For example:
@AfterClass
public void tearDown() throws Exception {
// Some test generated the *expected* error: CWWK1234E: The server blew up because blah blah blah
server.stopServer("CWWK1234E"); // Stop the server and indicate the 'CWWK1234E' error message was expected
}
If a test generates an FFDC that is expected to occur, use the @ExpectedFFDC
annotation. For example:
@Test
@ExpectedFFDC("java.lang.IllegalStateException")
public void someErrorPathTest() {
// Do something that causes a java.lang.IllegalStateException
}
If a test generates an FFDC sometimes and we decide it's OK to allow, use the @AllowedFFDC
annotation. Obviously, this annotation should be used sparingly, and @ExpectedFFDC
should be used instead whenever possible. An example usage would be:
@Test
@AllowedFFDC // allows any FFDCs to occur
public void someNastyErrorPathTest() {
// Do something that could cause a variety of error paths to occur
}
@Test
@AllowedFFDC("java.lang.IllegalStateException")
public void someErrorPathTest() {
// Do something that *sometimes* causes a java.lang.IllegalStateException
}
OpenLiberty allows running with Java 2 Security (j2sec) enabled, and so the buildandrun
task runs with j2sec enabled by default to test this scenario.
This can be disabled by using -Dglobal.java2.sec=false
, for example:
./gradlew build.example_fat:buildandrun -Dglobal.java2.sec=false
Even if there are only a few distinct j2sec issues, the log files can quickly become overwhelming due to the volume of logs per issue multiplied by the potential frequency of issues. It also can be a slow, iterative process to fix the issues one at a time.
To speed up this process, you can use the following -D property combinations:
./gradlew build.example_fat:buildandrun -Dglobal.java2.sec=false -Dglobal.debug.java2.sec=true
This will log j2sec issues, but "catch" the exception and continue execution of the test bucket (to flush out the issues more quickly).
In this case, a report will be generated for each server instance in build/libs/autoFVT/ACEReport-<timestamp>.log
, and even more detailed information about the full list of AccessControlException(s) will be contained within each of these server's console logs.
The -D properties above cause the test framework to generate properties within each server's ${server.config.dir}/bootstrap.properties
file, and so the properties in this file can be edited directly by the experienced user (as in the example below). Note because of the test framework writing to these properties, simply editing the bootstrap.properties
in the test source will not necessarily help (since the framework will add its own values). See this Knowledge Center article for more info.
If a server should NOT run with java 2 security ever
Add this to the ${server.config.dir}/bootstrap.properties
file for the server you want to permanently disable java 2 security for:
websphere.java.security.exempt=true
When we have multiple versions of a feature (e.g. cdi-1.2
and cdi-2.0
), it is useful to re-run the same tests on the newer feature levels (e.g. re-run the CDI 1.2 tests for both cdi-1.2
and cdi-2.0
) in order to prove that functionality that worked in previous feature versions continues to work in newer feature versions.
For example, if a FAT was written with Java EE 7 features, and you want to re-run the FAT with Java EE 8 equivalent features, you can do the following:
@RunWith(Suite.class)
@SuiteClasses({ ... })
public class FATSuite {
// Using the RepeatTests @ClassRule in FATSuite will cause all tests in the FAT to be run twice.
// First without any modifications, then again with all features in all server.xml's upgraded to their EE8 equivalents.
@ClassRule
public static RepeatTests r = RepeatTests.withoutModification()
.andWith(FeatureReplacementAction.EE8_FEATURES());
}
Note that when using the FeatureReplacementAction.EE8_FEATURES
action, the additional test iteration will be skipped automatically if the java level is less than Java 8.
For a more fine-grained approach, the RepeatTests
class rule can also be applied to individual test classes, for example:
@RunWith(FATRunner.class)
public class SomeTest extends FATServletClient {
// Runs all tests in 'SomeTest' first without modification, and then again after upgrading
// publish/servers/someServer/*.xml to use EE8 features
@ClassRule
public static RepeatTests r = RepeatTests.withoutModification()
.andWith(FeatureReplacementAction.EE8_FEATURES()
.forServers("someServer"));
Additionally, it is possible to exclude individual test methods or classes from a given repeat phase using the @SkipForRepeat
annotation. For example:
@Test
@SkipForRepeat(SkipForRepeat.NO_MODIFICATION)
public void testEE8Only() throws Exception {
// This test will skip for the EE7 feature (i.e. NO_MODIFICATION) iteration
}
@Test
@SkipForRepeat(SkipForRepeat.EE8_FEATURES)
public void testEE7Only() throws Exception {
// This test will skip for the EE8 feature iteration
}
And finally, for maximum level of flexibility, it is possible to write custom actions by implementing the RepeatTestAction
interface. For example, a FAT could be repeated with multiple database types in the following way:
public class RepeatTestsWithDB2 implements RepeatTestAction {
public RepeatTestsWithDB2(/* DB2 database info */) { }
@Override
public boolean isEnabled() { /* check if a DB2 database is available for use */ }
@Override
public void setup() { /* get and modify server.xml's to use DB2 database config */ }
}
And a custom action could be used as:
@ClassRule
public static RepeatTests r = RepeatTests.withoutModification()
.andWith(new RepeatTestsWithDB2(/* db2 database info */));
If helpful here is the componenttest.rules.repeater package source