Using separate database for unit tests in Play Framework


Programming / Tuesday, December 20th, 2016

The Play Framework (Java version, at least) is great and modern and all that, but recently I went through a lot of struggle to get evolutions and unit testing of models right. Here’s what causes the fundamental friction:

  • There’s no easy to read one database’s evolutions and apply them to the test database.
  • There’s no easy to tell Ebean to select a different database for unit tests.
  • When unit tests are run, Play doesn’t start the application, and hence, the Ebean server, by default.

Doesn’t look like something big is being lost, does it? But let me tell you what was lost: a few hours of sanity. Having worked (whatever little) with Django and Phoenix in the past, I’m used to simply saying something like ./manage.py test and watch as my tests get executed and the main database remains untouched. These tools go out of their way to create a test database at runtime, apply the migrations, do the necessary integrations, and destroy them after the tests are done. I wish play framework had given more thought to smoother development.

Anyway, the aim of this post is not so much to rant but to share a solution that worked for me. There are two major problems that we need to tackle.

Problem 1: The Ebean server is not available in tests

If you write a test like this in the Play Framework, be prepared for the building to crumble:

@Test
public void testAsiaKolkataTimeZoneHasId250() {
    TimeZone tz = TimeZone.find.byId(250L);
    Assert.assertNotNull(tz);
    Assert.assertEquals(tz.name, "Asia/Kolkata");
}

Ebean will not be loaded, and you will get a NullPointerException.

Why is that? The reason is that Ebean, routers, etc., are launched along with the application when you type¬†activator run. So here’s what you do to ensure this: Create a new base class for your unit tests, which will ensure that the application (made available in Play via Helpers.fakeApplication()) will be ready before your tests are run. I lost the link to the page that described this idea, but here’s the class:

import org.junit.*;
import play.Application;
import play.test.Helpers;

public class BaseModelTest {
    public static Application app;

    @BeforeClass
    public static void startApp() {
        app = Helpers.fakeApplication();
        Helpers.start(app);
    }

    @AfterClass
    public static void stopApp() {
        Helpers.stop(app);
    }
}

Now, any test you write that involves models will derive from this class:

import models.*;
import org.junit.*;

public class UniversalSettingsTest extends BaseModelTest {
    
    @Test
    public void testAsiaKolkataTimeZoneHasId250() {
        TimeZone tz = TimeZone.find.byId(250L);
        Assert.assertNotNull(tz);
        Assert.assertEquals(tz.name, "Asia/Kolkata");
    }
}

Hurray!

We should celebrate, but not so fast. There’s one more imp lurking in the details. If you create a new model instance and save it, it will land in your default database! Yes, all the unit tests will likely leave your database hopelessly muddy. Even after searching long and hard, I was unable to find a sane way to unit test on the same database.

The solution seems obvious. Create another database called test in the config file:

db {
  default.driver = org.postgresql.Driver
  default.url = "jdbc:postgresql://"${?FU_MAIN_DB_HOST}":"${?FU_MAIN_DB_PORT}"/"${?FU_MAIN_DB_NAME}
  default.username = ${?FU_MAIN_DB_USERNAME}
  default.password = ${?FU_MAIN_DB_PASSWORD}

  default.logSql = true

  # DB for running unit tests
  test.driver = org.postgresql.Driver
  test.url = "jdbc:postgresql://"${?FU_MAIN_TEST_DB_HOST}":"${?FU_MAIN_TEST_DB_PORT}"/"${?FU_MAIN_TEST_DB_NAME}
  test.username = ${?FU_MAIN_TEST_DB_USERNAME}
  test.password = ${?FU_MAIN_TEST_DB_PASSWORD}
}

What about evolutions for this database? Well, if you just create another folder called conf/evolutions/test and remember to keep copying new evolutions to over there, Play will make you upgrade the test database also before you launch your application. This is good, because it means that we won’t have to remember to apply evolutions ourself!

Problem 2: The default database gets selected by Ebean every time

This one is much simpler to tackle in comparison. And this time, I also have the link to the original post! But just in case that page gets deleted or something, here’s the recipe.

The idea is to create a separate configuration file, which will overwrite the default database when in testing mode! To achieve this, I created an application.test.conf with the following contents:

include "application.conf"

db.default.driver = org.postgresql.Driver
db.default.url = "jdbc:postgresql://"${?FU_MAIN_TEST_DB_HOST}":"${?FU_MAIN_TEST_DB_PORT}"/"${?FU_MAIN_TEST_DB_NAME}
db.default.username = ${?FU_MAIN_TEST_DB_USERNAME}
db.default.password = ${?FU_MAIN_TEST_DB_PASSWORD}

Then, in the build.sbt, I added at the end:

javaOptions in Test += "-Dconfig.file=conf/application.test.conf"

And that’s it!

I’m sure there are neater ways, perhaps even “recommended” ones, but I was unable to find them. With this setup, I have overcome all the challenges and can look forward to peace of mind with unit testing. It’s hard to believe that setting this up took me around a week to figure out. Of course it’s my fault that I’m new to Play and am not smart in general, but I really, really think that a framework should be more helpful.

2 Replies to “Using separate database for unit tests in Play Framework”

  1. Have you tried the same with h2 in memory data base. instead of having a dedicated database allocated for tests. we may have an inmemory database for tests isnt it.

    1. Hey Vidhey, sorry for the late response! No, I haven’t tried H2 or other in-memory databases but I think it’s a great idea. The only catch I see is that the in-memory database should be able to simulate all the functionality your app relies on, so I tend to avoid it.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.