GraphAware Blog

Find out what's new in the Neo4j world

GraphUnit: Testing Neo4j Code

Neo4j GraphAware Intermediate GraphUnit Testing 29 May 2014 by Michal Bachman

Recently, we announced the GraphAware Framework. Today, I would like to introduce its first feature called GraphUnit. GraphUnit is a component that helps Java developers unit test their code that talks to Neo4j and mutates data.

Unit Testing Neo4j Code

When writing Java code that modifies data stored in Neo4j, developers can use the ImpermanentGraphDatabase in conjunction with any of APIs provided by Neo4j to test that code. This includes the native Java API, the traversal framework, and Cypher. (I’ve excluded the REST API because using that to unit test Java code wouldn’t make much sense.)

Let’s say we’re testing code that creates two nodes in the database with some labels, connects them with a relationship, and sets some properties on both the nodes and the relationship. A good unit test of such functionality would actually assert a couple of things:

  • that the expected nodes and relationships were created and the labels and properties correctly set
  • that no other nodes or relationships were created, and that no additional properties or labels were set
  • that existing parts of the graph remained untouched

Since Cypher is primarily built for declarative (rather than imperative) operations on the database, it would be pretty hard to use it for fulfilling any but the first requirement of our unit test. In other words, asserting that something in the database exists is pretty simple, but checking that no extra nodes were created or that the created nodes do not have extra labels or properties would be pretty hard work. The traversal framework, which we’re not going to discuss further, suffers from many of the same limitations for this use case.

We are thus left with asserting the state of our graph using native low-level Java APIs. Indeed, this is the approach people usually take when unit testing their Neo4j code. However, it is still a lot of work to assert that nodes and relationships don’t have extra properties, that there are no extra nodes or relationships, etc. Moreover, the Java API, when used for unit testing, isn’t very readable.

GraphUnit

GraphUnit (Javadoc | code) addresses the problems outlined above. It gives developers the opportunity to express the desired state of the graph using Cypher and assert that it is, indeed, the case. The following method is used for that purpose:

public static void assertSameGraph(GraphDatabaseService database, String sameGraphCypher)

The first parameter is the database, the state of which is being asserted. The second parameter is a Cypher (typically CREATE) statement expressing the desired state of the database. The graph in the database and the graph that would be created by the provided Cypher statement must be identical in order for the unit test to pass. Of course, internal Neo4j node and relationship IDs are excluded from any comparisons.

For tests on graphs that are large and it is not the developer’s intention to verify the state of the entire graph, GraphUnit provides another method:

public static void assertSubgraph(GraphDatabaseService database, String subgraphCypher)

The idea is the same, except there can be additional relationships and nodes in the database that are not expressed in the Cypher statement. But the Cypher-defined subgraph must be present the database with exactly the same node labels, relationship types, and properties on both nodes and relationships, in order for the test to pass.

Using GraphUnit

Feel like giving it a try? Add the following snippet to your pom.xml (assuming you’re using Maven) and you’re good to go.

<dependency>
    <groupId>com.graphaware.neo4j</groupId>
    <artifactId>tests</artifactId>
    <version>2.0.3.5</version>
    <scope>test</scope>
</dependency>

Please note that we’re not going to be updating the version number in this post with every new release, so check here for the latest version.

Examples

Just to illustrate some of the points made earlier, here is an example from our own development (the GraphAware TimeTree, which we’ll write about another time).

Prior to using GraphUnit, a unit test using Neo4j Java API looked like this.

@Test
public void trivialTreeShouldBeCreatedWhenFirstDayIsRequested() {
    //Given
    long dateInMillis = dateToMillis(2013, 5, 4);

    //When
    Node dayNode;
    try (Transaction tx = database.beginTx()) {
        dayNode = timeTree.getInstant(dateInMillis, Resolution.DAY);
        tx.success();
    }

    //Then
    try (Transaction tx = database.beginTx()) {
        assertEquals(4, count(GlobalGraphOperations.at(database).getAllNodes()));

        assertTrue(dayNode.hasLabel(TimeTreeLabels.DAY));
        assertEquals(4, dayNode.getProperty(VALUE_PROPERTY));

        Node monthNode = dayNode.getSingleRelationship(CHILD, INCOMING).getStartNode();
        assertEquals(monthNode, dayNode.getSingleRelationship(FIRST, INCOMING).getStartNode());
        assertEquals(monthNode, dayNode.getSingleRelationship(LAST, INCOMING).getStartNode());
        assertTrue(monthNode.hasLabel(TimeTreeLabels.MONTH));
        assertEquals(5, monthNode.getProperty(VALUE_PROPERTY));
        assertNull(dayNode.getSingleRelationship(NEXT, INCOMING));
        assertNull(dayNode.getSingleRelationship(NEXT, OUTGOING));

        Node yearNode = monthNode.getSingleRelationship(CHILD, INCOMING).getStartNode();
        assertEquals(yearNode, monthNode.getSingleRelationship(FIRST, INCOMING).getStartNode());
        assertEquals(yearNode, monthNode.getSingleRelationship(LAST, INCOMING).getStartNode());
        assertTrue(yearNode.hasLabel(TimeTreeLabels.YEAR));
        assertEquals(2013, yearNode.getProperty(VALUE_PROPERTY));
        assertNull(yearNode.getSingleRelationship(NEXT, INCOMING));
        assertNull(yearNode.getSingleRelationship(NEXT, OUTGOING));

        Node rootNode = yearNode.getSingleRelationship(CHILD, INCOMING).getStartNode();
        assertEquals(rootNode, yearNode.getSingleRelationship(FIRST, INCOMING).getStartNode());
        assertEquals(rootNode, yearNode.getSingleRelationship(LAST, INCOMING).getStartNode());
        assertTrue(rootNode.hasLabel(TimeTreeLabels.ROOT));
        assertNull(rootNode.getSingleRelationship(NEXT, INCOMING));
        assertNull(rootNode.getSingleRelationship(NEXT, OUTGOING));
        assertNull(rootNode.getSingleRelationship(FIRST, INCOMING));
        assertNull(rootNode.getSingleRelationship(LAST, INCOMING));
    }
}

After switching to GraphUnit, the same test looks like this:

@Test
public void trivialTreeShouldBeCreatedWhenFirstDayIsRequested() {
    //Given
    long dateInMillis = dateToMillis(2013, 5, 4);

    //When
    Node dayNode;
    try (Transaction tx = database.beginTx()) {
        dayNode = timeTree.getInstant(dateInMillis, Resolution.DAY);
        tx.success();
    }

    //Then
    GraphUnit.assertSameGraph(database, "CREATE" +
            "(root:TimeTreeRoot)," +
            "(root)-[:FIRST]->(year:Year {value:2013})," +
            "(root)-[:CHILD]->(year)," +
            "(root)-[:LAST]->(year)," +
            "(year)-[:FIRST]->(month:Month {value:5})," +
            "(year)-[:CHILD]->(month)," +
            "(year)-[:LAST]->(month)," +
            "(month)-[:FIRST]->(day:Day {value:4})," +
            "(month)-[:CHILD]->(day)," +
            "(month)-[:LAST]->(day)");

    try (Transaction tx = database.beginTx()) {
        assertTrue(dayNode.hasLabel(TimeTreeLabels.Day));
        assertEquals(4, dayNode.getProperty(VALUE_PROPERTY));
    }
}

The benefits in terms of readability are clear. Moreover, the GraphUnit version of the test is actually a better unit test, because it fails when there are extra things in the graph that aren’t explicitly mentioned in the Cypher statement. This isn’t the case in the first version of the test, so bugs could easily be missed out.

Conclusion

GraphUnit is a simple open-source library for testing code that mutates the Neo4j database. It helps to make the tests better and more readable. It is a small part of the GraphAware Neo4j Framework, other components of which will be introduced in coming blog posts. Over and out!

Share this blog post:

comments powered by Disqus