By

Erreur de Transaction SQL lors d'un test Fonctionnel avec Play!>

Je viens d’être confronté à un problème lors de l’exécution des tests Fonctionnels d’une application Play!> avec la base de données MySQL (alors qu’avec H2 le problème n’apparaît pas).

Sur un projet Play!> 1.2.5 avec MySQL (InnoDB), j’ai souhaité réaliser un test fonctionnel, pourtant très simple (un model User, un controller et une classe de test du controller), et pourtant j’ai été confronté à l’erreur suivante :

A java.lang.RuntimeException has been caught, java.util.concurrent.ExecutionException: 
play.exceptions.JavaExecutionException: 
Try to read id on null object models.User (controllers.Application.index, line 11)

Visiblement, lors de l’exécution du tests fonctionnel, la couche Controller ne semblait pas trouver de résultat, alors que la couche de test en trouvait…

Je vais essayer de vous montrer ici mon analyse du problème et une solution pour le contourner.

Pour les plus flemmard, voici un zip du projet.

Avant toute chose, créons une base de données (et un utilisateur) sous MySQL :

$ mysql -u root -p
Enter password: 

mysql> CREATE DATABASE fixtureFail;
Query OK, 1 row affected (0.01 sec)

mysql> CREATE USER 'fixtureFail'@'%' IDENTIFIED BY 'fixtureFail';
Query OK, 0 rows affected (0.00 sec)

mysql> GRANT ALL ON fixtureFail.* TO 'fixtureFail'@'%' IDENTIFIED BY 'fixtureFail';
Query OK, 0 rows affected (0.07 sec)

Puis créons une application Play!> standard:

$ play new fixtureFail

Modifions quelque peut le fichier de configuration : application.conf

# Dev
# ~~~~~
application.name=fixtureFail
application.mode=dev
application.secret=NarifLkF6ynvKppI0Rw36mxKpiEP9ThPeHKFPQ79tH9CPukWMuIVZZByHfHFcmIO
application.log=DEBUG
date.format=yyyy-MM-dd
attachments.path=data/attachments
db=mem
mail.smtp=mock

# Testing
# ~~~~~
%test.application.mode=dev
%test.jpa.ddl=update
%test.mail.smtp=mock
#%test.db.url=jdbc:h2:mem:play;MODE=MYSQL;LOCK_MODE=0
%test.db.url=jdbc:mysql://localhost/fixtureFail
%test.db.driver=com.mysql.jdbc.Driver
%test.db.user=fixtureFail
%test.db.pass=fixtureFail

Et codons ensuite les 3 classes qui nous interessent :

app.models.User

package models;

import play.db.jpa.Model;
import javax.persistence.Entity;

@Entity
public class User extends Model {
    public String username;
    public String email;
}

app.controllers.Application

package controllers;

import models.User;
import play.Logger;
import play.mvc.Controller;

public class Application extends Controller {
    public static void index() {
        User user = User.find("username=?", "nicogiard").first();
        Logger.debug("user from controller : %s", user.id);
        render();
    }
}

test.ApplicationTest

import models.User;
import org.junit.BeforeClass;
import org.junit.Test;
import play.Logger;
import play.mvc.Http.Response;
import play.test.Fixtures;
import play.test.FunctionalTest;

public class ApplicationTest extends FunctionalTest {

    @BeforeClass
    public static void before() {
        Logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~");
        Fixtures.deleteAllModels();
        Fixtures.loadModels("initial-data.yml");

        User user = User.find("username=?", "nicogiard").first();
        Logger.debug("user from before : %s", user.id);
    }

    @Test
    public void testThatIndexPageWorks() {
        User user = User.find("username=?", "nicogiard").first();
        Logger.debug("user from test : %s", user.id);

        Response response = GET("/");
        assertIsOk(response);
        assertContentType("text/html", response);
        assertCharset("utf-8", response);
    }
}

Lançons ensuite les tests. Nous nous attendons à avoir un résultat (dans la console) proche de celui ci :

user from before : 1
user from test : 1
user from controller : 1

Mais pour une raison que je n’explique pas trop voilà le résultat obtenu :

$ play test
...
11:21:21,922 INFO  ~ Application 'fixtureFail' is now started !
11:21:29,287 DEBUG ~ ~~~~~~~~~~~~~~~~~~~~~~~~
11:21:29,791 DEBUG ~ user from before : 1
11:21:29,805 DEBUG ~ user from test : 1

Et le Test échoue lamentablement…

Alors qu’en base de données la donnée existe bien.

mysql> select * from User;
+----+---------------------+-----------+
| id | email               | username  |
+----+---------------------+-----------+
|  1 | nicogiard@gmail.com | nicogiard |
+----+---------------------+-----------+
1 row in set (0.00 sec)

Le problème semble être que deux transactions indépendantes sont en cours (une pour le test et une pour le controller) ou quelque chose du genre.

Je vous avoue que je n’ai pas encore eu beaucoup de temps pour fouiller dans le code source de Play!> pour chercher d’où cela peut venir, mais j’ai au moins une solution pour pallier au problème

Vous devez donc modifier le mode de chargement des données initiales dans votre test fonctionnel avec le code suivant : test.ApplicationTest

...
    @BeforeClass
    public static void before() throws ExecutionException, InterruptedException {
        Logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~");
        new Job() {
            @Override
            public void doJob() {
                Fixtures.deleteAllModels();
                Fixtures.loadModels("initial-data.yml");
            }

        }.now().get();

        User user = User.find("username=?", "nicogiard").first();
        Logger.debug("user from before : %s", user.id);
    }
...

En effet, le chargement au sein d’un Job semble contourner le problème et permettre d’exécuter le test et d’obtenir le résultat escompté.

$ play test
11:26:20,386 INFO  ~ Application 'fixtureFail' is now started !
11:26:26,574 DEBUG ~ ~~~~~~~~~~~~~~~~~~~~~~~~
11:26:27,120 DEBUG ~ user from before : 1
11:26:27,126 DEBUG ~ user from test : 1
11:26:27,141 DEBUG ~ user from controller : 1

Et maintenant le test passe

Si jamais l’un d’entre vous avait ne serait-ce qu’une idée, n’hésitez pas à partager avec nous !