Specs2

Acolyte can be used with specs2 to write executable specification for function accessing persistence.

Considering a sample persistence function:

object Zoo {
  trait Animal { def name: String }
  case class Bird(name: String, fly: Boolean = true) extends Animal
  case class Dog(name: String, color: String) extends Animal

  def atLocation(con: java.sql.Connection)(id: Int): Option[Animal] = {
    // Yes would be better with something like Anorm or Slick
    var stmt: java.sql.PreparedStatement = null
    var rs: java.sql.ResultSet = null

    try {
      stmt = con.prepareStatement("SELECT * FROM zoo WHERE location = ?")
      stmt.setInt(1, id)

      rs = stmt.executeQuery()
      rs.next()

      rs.getString("type") match {
        case "bird" => Some(Bird(rs.getString("name"), rs.getBoolean("fly")))
        case "dog"  => Some(Dog(rs.getString("name"), rs.getString("color")))
        case _      => None
      }
    } catch {
      case _: Throwable => sys.error("Fails to locate animate")
    } finally {
      try { rs.close() } catch { case _: Throwable => }
      try { stmt.close() } catch { case _: Throwable => }
    }
  }
}

Then following specification can be written, checking that query result is properly selected and mapped:

import acolyte.jdbc.Implicits._
import acolyte.jdbc.AcolyteDSL.{ // DSL
  connection, handleQuery, withQueryResult
} 
import acolyte.jdbc.RowLists.rowList5
import Zoo._

object ZooSpec extends org.specs2.mutable.Specification {
  val zooSchema = rowList5(
    classOf[String] -> "type",
    classOf[Int] -> "location",
    classOf[String] -> "name",
    classOf[Boolean] -> "fly",
    classOf[String] -> "color")

  "Dog" should {
    "be found at location 1, and be red" in withQueryResult(
      zooSchema :+ ("dog", 1, "Scooby", null.asInstanceOf[Boolean], "red")) {
        conn =>
          atLocation(conn)(1) aka "animal" must beSome(Dog("Scooby", "red"))
    }
  }

  "Ostrich" should {
    "be found at location 2" in {
      val conn = connection(handleQuery { _ =>
        zooSchema :+ ("bird", 2, "Ostrich", false, null.asInstanceOf[String])
      })

      atLocation(conn)(2) aka "animal" must beSome(Bird("Ostrich", false))
    }
  }
}

JUnit

Acolyte can be used with JUnit to write test case for Java method accessing persistence:

import java.util.List;

import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Connection;

import static org.junit.Assert.*;

import acolyte.jdbc.AbstractCompositeHandler.QueryHandler;
import acolyte.jdbc.StatementHandler.Parameter;
import acolyte.jdbc.QueryResult;
import acolyte.jdbc.RowList5;

import static acolyte.jdbc.RowList.Column;
import static acolyte.jdbc.RowLists.rowList5;

@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)
public class ZooTest {
    private RowList5<String,Integer,String,Boolean,String> zooSchema =
        rowList5(Column(String.class, "type"),
                 Column(Integer.class, "location"),
                 Column(String.class, "name"),
                 Column(Boolean.class, "fly"),
                 Column(String.class, "color"));

    @org.junit.Test
    public void dogAtLocation() {
        final String handlerId = "dogTest";
        acolyte.jdbc.Driver.
            register(handlerId, acolyte.jdbc.CompositeHandler.empty().
                     withQueryDetection("^SELECT").
                     withQueryHandler(new QueryHandler() {
                             public QueryResult apply(String sql, List<Parameter> parameters) throws SQLException {
                                 return zooSchema.
                                     append("dog", 1, "Scooby", null, "red").
                                     asResult();

                             }
                         }));

        final String jdbcUrl = 
            "jdbc:acolyte:anything-you-want?handler=" + handlerId;

        try {
            final Zoo zoo = new Zoo(DriverManager.getConnection(jdbcUrl));
            
            org.junit.Assert.assertEquals("Dog should be found at location 1", 
                                          zoo.atLocation(1), 
                                          new Dog("Scooby", "red"));

        } catch (Exception e) {
            org.junit.Assert.fail(e.getMessage());
        }
    }

    @org.junit.Test
    public void birdAtLocation() {
        final Connection conn = acolyte.jdbc.Driver.
            connection(acolyte.jdbc.CompositeHandler.empty().
                     withQueryDetection("^SELECT").
                     withQueryHandler(new QueryHandler() {
                             public QueryResult apply(String sql, List<Parameter> parameters) throws SQLException {
                                 return zooSchema.
                                     append("bird", 2, "Ostrich", false, null)).
                                     asResult();

                             }
                         }));

        org.junit.Assert.assertEquals("Ostrich should be found at location 2", 
                                      new Zoo(conn).atLocation(2), 
                                      new Bird("Ostrich", false));

    }
}

Sample zoo method could be as following:

import java.sql.PreparedStatement;
import java.sql.Connection;
import java.sql.ResultSet;

public class Zoo {
    private final Connection connection;

    public Zoo(Connection c) { this.connection = c; }

    public Animal atLocation(int id) {
        PreparedStatement stmt = null;
        ResultSet rs = null;

        try {
            stmt = this.connection.
                prepareStatement("SELECT * FROM zoo WHERE location = ?");

            stmt.setInt(1, id);

            rs = stmt.executeQuery();
            rs.next();

            final String type = rs.getString("type");
            
            if (type == "bird") {
                return new Bird(rs.getString("name"), rs.getBoolean("fly"));
            } else if (type == "dog") {
                return new Dog(rs.getString("name"), rs.getString("color"));
            } else {
                return null;
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("Fails to locate animate");
        } finally {
            try { rs.close(); } catch (Exception e) {}
            try { stmt.close(); } catch (Exception e) {}
        }
    }
}

interface Animal {
    public String getName();
}
class Bird implements Animal {
    public final String name;
    public final boolean fly;

    public Bird(String n, boolean f) {
        this.name = n;
        this.fly = f;
    }

    public String getName() { return this.name; }

    public boolean equals(Object o) {
        if (o == null || !(o instanceof Bird)) {
            return false;
        } 

        final Bird other = (Bird) o;

        return (((this.name == null && other.name == null) ||
                 (this.name != null && this.name.equals(other.name))) &&
                this.fly == other.fly);

    }
}
class Dog implements Animal {
    public final String name;
    public final String color;

    public Dog(String n, String c) {
        this.name = n;
        this.color = c;
    }

    public String getName() { return this.name; }

    public boolean equals(Object o) {
        if (o == null || !(o instanceof Dog)) {
            return false;
        } 

        final Dog other = (Dog) o;

        return (((this.name == null && other.name == null) ||
                 (this.name != null && this.name.equals(other.name))) &&
                ((this.color == null && other.color == null) ||
                 (this.color != null && this.color.equals(other.color))));

    }
}

Anorm

Acolyte is useful to write test about persistence in projects using Anorm.

Read the 10 minutes tutorial about Acolyte with Anorm.

Play Framework

Acolyte can be easily used with Play test helpers.

First step is to create an application builder:

import play.api.inject.guice.GuiceApplicationBuilder

import acolyte.jdbc.StatementHandler

def fakeAppBuilder(
  h: Option[StatementHandler] = None
): GuiceApplicationBuilder => GuiceApplicationBuilder = { initial =>
  val builder = initial.load(
    new play.api.i18n.I18nModule(),
    new play.api.mvc.CookiesModule(),
    new play.api.inject.BuiltinModule()
  )

  h match {
    case Some(handler) => {
      val id = System.identityHashCode(this).toString
      acolyte.jdbc.Driver.register(id, handler)

      builder.configure(
        "db.default.driver" -> "acolyte.jdbc.Driver",
        "db.default.url" -> s"jdbc:acolyte:test?handler=${id}"
      )
    }

    case _ => builder
  }
}

Then Play/DB test can be performed as following:

import acolyte.jdbc.{ AcolyteDSL, QueryResult }, AcolyteDSL.handleStatement

lazy val handler = Some(handleStatement.
  withQueryDetection("^SELECT").withQueryHandler { e =>
    QueryResult.Nil // Any Acolyte result
  })

play.api.test.Helpers.running(fakeAppBuilder(handler)) { app =>
  val db = app.injector.instanceOf[play.api.db.Database]

  db.withConnection { con =>
    // Connection |con| will use provided |handler|
    // So any DB related test can be done there
  }
}

This can be simplified using the Acolyte Play JDBC module:

import acolyte.jdbc.{ AcolyteDSL, QueryResult }
//import acolyte.jdbc.Implicits._
import acolyte.jdbc.play.{ PlayJdbcContext, PlayJdbcDSL }

val playHandler = AcolyteDSL.handleStatement.
  withQueryDetection("^SELECT").withQueryHandler { e =>
    QueryResult.Nil // Any Acolyte result
  }

def playCtx: PlayJdbcContext = PlayJdbcDSL.withPlayDB(playHandler)

playCtx { db: play.api.db.Database =>
  ??? // the code to be tested with a Play DB
}

This module can be configured in your Play project as bellow.

libraryDependencies ++= Seq(
  "org.eu.acolyte" %% "play-jdbc" % "1.2.7" % "test"
)

See online API documentation