What is a test fixture

Regarding to the ScalaTest documentation:

A test fixture is composed of the objects and other artifacts (files, sockets, database connections, etc.) tests use to do their work. When multiple tests need to work with the same fixtures, it is important to try and avoid duplicating the fixture code across those tests. The more code duplication you have in your tests, the greater drag the tests will have on refactoring the actual production code.

The test fixture might be used to prepare the whole context required by the test case.

Initialize repositories, services, actor system, database connections etc.

Generate the test data.

Tear down the app, disconnect the database, or take another actions after the test.

In other words, the test fixture is a way to connect your tests with required part of the application (in most cases — the integration tests).

The chocolate factory

Let’s consider a demo application of chocolate factory.

In this simple example the Chocolate consists of the ChocolateBar and the Packaging . There is also the AdService to provide the text printed on the Packaging and the PackagePrinter to produce the actual Packaging for the given size of ChocolateBar . Finally, there is a ChocolateFactory which handles the whole process. It produces the ChocolateBar , calls PackagePrinter for the Packaging and returns the Chocolate .

Even in this simple example initializing a test case might take a few lines of code:

When the ChocolateFactory initialization changes, or when some teardown actions are required, you need to change the code in every test case.

1. Using withFixture from ScalaTest

ScalaTest provides a few base test classes to handle fixtures (for example org.scalatest.fixture.FlatSpec that might be used instead of org.scalatest.FlatSpec ). They require the implementation of withFixture method to prepare your fixture. Besides, you need to provide the definition of FixtureParam type (either implement a class with the name FixtureParam or make a type alias).

This approach has some drawbacks. You need to make additional efforts to share fixtures across multiple test suites and, what is more problematic, there is no easy way to call withFixture for different, test-case-specific parameters. You may eventually make the FixtureParam an alias to some function type, but it seems to be complicating both the withFixture and the test case code.

2. Using traits

Another approach uses custom traits that define the required parts of the application. It might be useful especially in cases when your application is defined in trait modules or when you want to define parts of the fixture in the separate traits and mix them appropriately for specific test cases.

In this example a fixture is defined in the TestApp trait and you need to create the TestApp object for each test with a very nice syntax. Even the code when you mix multiple traits is concise, for example: ... in new TestApp with DemoData { . Besides, you don’t need to call the test explicitly in your fixture. The fixture is just an (independent) part of the application.

This approach has some significant drawbacks as well. At the end of the day you may face a complex trait structure for the fixture, difficult to reason about. You need to know which services are provided by the given fixture, because they are not expressed directly in the test — there are just some values in the parent trait.

There is no easy way to parametrize the test cases. You may provide additional abstract values for a trait with a risk of random NullPointerException because of broken val initialization in complex class structures in Scala compiler.

Finally, there is no easy way to implement the teardown in this kind of fixtures.

3. Using functions

You may use functions to define your test fixtures as well. The body of a fixture function is very similar in this case to withFixture function from ScalaTest, however there are some small differences and big advantages over this approach.

But let’s start with the example:

First of all, you control the declaration of the fixture function. As you can see in the example above, you can add some additional parameters and provide your own function name. You just need to keep the test: Fixture => Unit parameter to be able to execute the test.

Besides, you can easily share fixture functions across multiple test suites. You can use multiple fixtures within a single suite as well. Free from base class conventions, this is a very flexible approach.

The only weakness, in contrast to the other two approaches is the ability to mix multiple fixtures. There is no simple mechanism like trait mix-in, you need to use the other fixtures explicitly or invent a new way of combining them.