Pick the module you need:
testCompile 'net.piotrturski.testegration:core:0.0.2-beta'
testCompile 'net.piotrturski.testegration:postgres:0.0.2-beta'
-
core: provides lifecycle, yaml, docker
-
postgres: includes core and provides postgres supoprt
Currently Testegration is marked as beta. It doesn’t mean it’s unstable. It means that API is most probably going to change. However, if you follow our guides and encapsulate the usage of our API, any changes should affect only one of your classes.
Testegration aims to allow easy to set-up, debuggable, fast, hassle-free, local integration tests on jvm.
The core concept in Testegration is Definition
class.
It provides a lifecycle that aims to be flexible enough to describe most popular
integration test environments.
And it lets you provide your actions for each phase of this lifecycle.
Once you specified all the desired behavior, you can run such definition before each
test and expect your environment to be ready for that test.
One important strength of provided API is ability to group definitions for a few phases.
Such building blocks are called PartialDefinition
. Why it’s so cool? Because it lets
you quickly build the definition of your testing environment by composing those blocks.
For example imagine you have two blocks: a block that starts any docker container and
a block that controls state of database of some specific vendor.
In this case you might be able to define your whole testing environment in a few
lines of code.
And what if you don’t like some very specific behavior of one of those building blocks? Then you can still use it with adding your logic or you can completely remove or replace some parts of it.
And that’s exactly what Testegration is trying to achieve.
Definition
class has plenty of methods but they all fall into a few categories listed below.
Most of those methods accept an action. This action will be fired at the specific lifecycle phase.
Most of those actions will receive some input: RuntimeStore
and/or configuration. Or EnvironmentRuntime
that encapsulates both of them. RuntimeStore
is a simple key-value store that allows you pass objects
between different lifecycle phases. Configuration will be described below.
- Configuration
-
configure
- action provided to this method will be called only once per the test suite. It can return any object. Returned object will be available to all other actions. Currently the configure action cannot be applied usingPartialDefinition
.
Definition<MyConfig> def =
Definition.<MyConfig>of()
.configure(() -> ...) // return MyConfig
.configure(runtimeStore -> { // or put anything into store
return ... // and return MyConfig
})
- Naming
-
label
- if you have only one definition, you can skip this. Otherwise eachDefinition
(representing different environment) must have different label. Label can be any object implement hashcode and equals. Currently the label cannot be applied usingPartialDefinition
.Label is used to compare environments (like
hashCode
andequals
). Thanks to labels it doesn’t matter if you create only one instance ofDefinition
and store it inside static variable or you create new instance at each 'before test' method execution. - Lifecycle modification
-
lifecycle phase + (
ε | prepend | append
)Examples:
postConfigurePrepend
,buildSchema
Let’s you modify a specific phase. You can append or prepend your action to already specified actions for that phase or completely replace all actions.
Usage:
Definition.of() .buildSchemaAppend(runtimeStore -> { Flyway flyway = new Flyway(); flyway.setDatasource(runtimeStore.getShared("datasource")); flyway.migrate(); });
- Partial definition
-
First
PartialDefinition
is simply a group of operations that will be applied toDefinition
. It’s just a functional interface with the method:void configure(Definition<Config> definition)
For example this is a valid
PartialDefinition
:PartialDefinition partial = definition -> definition .buildSchemaAppend(...) .postConfigurePrepend(...);
with
- modifies the lifecycle according to all the definitions inside the providedPartialDefinition
.Definition.of() .with(partial)
withC
- applies all the definitions insidePartialDefinition
returned by a function taking configuration as an input. [[ Work in progress: To be documented… ]] - Execution
-
run
- Using the providedDefinition
it prepares testing environment for the next test. So you have to call this method before each test. You also need to run this method before your testing framework needs the working environment. For example, in case of spring-test and JUnit, spring’s context is created before any@Before
method, therefore this method should also be called at@BeforeClass
. This method returns an object that lets you accessRuntimeStore
and configuration.You don’t have to define environment once and then call
run
many times on the sameDefinition
object. Environments are cached by label. So for convenience you can encapsulate environment definition and running in a method that will be called before each test.
- configure
-
here you can read / build and expose your configuration to other lifecycle methods.
- post configure
-
good place to announce your configuration to your testing framework
- connection check
-
your action should check if your environment is available and throw exception if not. Remember to clean after yourself - free all not needed resources.
- start environment
-
your action should start your environment here. And this environment should be detected by your
connection check
action. This action won’t be executed ifconnection check
phase found some connection. - full clean
-
after you connected to some environment, it may it may contain any garbage. This is the right place to completely clean it.
- build schema
-
this is a place where you can provision your environment: create all the structures required by your application.
- reset
-
this is the only phase that is executed at every single
run
call. Use it to clean environment after previous test. - close connector
-
this is your chance to release any resources before your environment shutdown.
- stop environment
-
your action should stop your environment started at
start environment
phase. This action won’t be executed ifstart environment
wan’t executed.
The Definition
API can be used to define most of integration
testing scenarios (see DynamoDB example), although sometimes it won’t be trivial.
That’s why Testegration comes with comprehensive support for some technologies.
Higher level building blocks can be composed to easily setup the environment you need.
If you don’t like some actions provided by those predefined blocks, you can always
modify them using standard Definition
methods.
The plan is to continuously add support for popular tools in future versions of Testegration.
ConfigLoader.fromYaml(clazz, filename)
will return a supplier (that can be used in a configure
phase).
That supplier reads filename.yml
file and deserializes it to class clazz
using jackson.
If file is missing, empty or some property is missing, object with default value will be returned.
It’s advised to use a no-arg constructor or otherwise ensure that class can be
instantiated even if there is no properties in a file.
The default file location is ${user.home}/.config/testegration/
but you can override it by setting testegration
environment variable to some
other directory.
Docker.run(command)
returns PartialDefinition
that uses docker container with specified options
and later stops the started container.
It appends to start environment
and stop environment
phases.
The command should contain all the options that can be provided to the
docker run
command, e.g
Docker.run("--name my_dynamo -p 7777:7777 tray/dynamodb-local -inMemory -port 7777")
Requires docker 1.13.0+ and ability to run docker run
without sudo
.
Postgres.docker("my-project", "9.6.1")
returns a Definition
with appended actions:
-
read a configuration from
my-project.yml
and deserialize it toPostgresConf
class. Therefore you can change the default port, user etc, in case you have your own postgres installed or the default port already used. -
name the environment
my-project
-
if needed start docker with postgres in version
9.6.1
and place "my-project" in the container’s name. The postgres will run on default or configured port. The docker will be stopped after tests -
checks if postgres is ready using port, user, schema etc specified in
PostgresConf
or overridden by yml file. -
provides empty tables before each test
That’s all you need to test a typical application that do its own migrations
(like a sprong-boot integration tests with flyway or liquibase).
If you run tests without any framework that does the migrations for you, you need to add
you migrations to build schema
phase.
If you want to reuse this whole definition in your own configuration, it’s provided (except configuration and naming - due to current limitations) in:
Postgres.dockerPartial(String projectName, String postgresVersion)
It returns a function that accepts PostgresConf
and returns PartialDefinition
.
If actions provided by our defaults are not enough for you, you can run your own sql queries using:
Postgres.execute(PostgresConf conf, String... commands)
Although provided defaults are designed to perform as little operations as possible and optimize time required to run multiple tests, still environment needs to be started once. During the development it’s a waste of time. To avoid it, just start your environment manually (e.g. copy paste the docker command-line). This way your tests will always connect to already existing environment and will not stop it after tests are finished. So it will be ready for the next testing cycle.
Still the full clean
and 'build schema' phases will be triggered.
That’s usually what you want when you work intensively or changing the schema.
If you want to disable those phases (e.g. while working on DML code only)
you have to do it programmatically on your own,
there is no out-of-the-box support for that… yet.
If some of your integration tests require more than one external system (e.g. LDAP and db),
you should simply create two different Definition
object (named differently)
and run each Definition
before the test that requires it.
In this repo you can find complete, working sources. All the examples run on travis, can be run from IDE or local command-line. Just like any other tests.
Most of those tests uses Spring and JUnit, but they show the general idea about how to configure tests using any dependency injection (DI) framework, no DI framework at all, any testing tool and any DB migration framework.
In case you use this tool in a completely different stack, feel free to notify us about it. It may be worth publishing in this doc.
This is one of the most standard configuration. Here you can find full application with Spring’s integration tests.
In this example Spring takes care about running Flyway’s migrations so our Definition doesn’t need it.
Of course it’s not the only way of connecting Testegration with Spring tests.
Another way would be to provide test dataSource in tests.
That test datasource would run
the Definition
before creation.
Here you will see spring boot running jpa-only integration tests. Including spring-data repositories.
Here you’ll find Spring’s app that uses jdbcTemplate to achieve some more complex behavior. You’ll find two types of tests:
-
Spring’s integration tests. With similar setup as in spring-hibernate example.
-
Standalone tests. They don’t create spring context and therefore start much faster.
Here you’ll find sample of standalone TestNg test with flyway.
There is no out-of-the-box support for DynamoDB (yet). Let’s see how you can test it.
[[ Work in progress: To be documented… ]]
Depending on the demand (votes), there is planned out-of-the-box support for:
-
Running application locally with development storage
-
Docker with
sudo
-
Docker Compose
-
Oracle Database
-
MySql
-
MsSql
-
MongoDB
-
DB2
-
Redis
-
Elasticsearch
-
Vagrant