Skip to main content
Blog

Testing with state using GraphWalker | Part 1 – Persistence

By 9 oktober 2019november 28th, 2019No Comments

In this first installment of Testing with state using GraphWalker, we’ll look at integration testing the persistence layer of a Spring application. So, our tests will target the database and the data repositories.

People trying to walk a path

Statefulness

The database itself is inherently stateful. As a result, if we connect a data repository to an actual database, it will also show stateful behavior. For example, when we query for a record that is not there, the result will be empty. Then, if we add the record and run the query again, the answer will be different and the record will be returned. Now, the fact that the queries result depended on previous actions, means that we are dealing with state.

In GraphWalker we can model this state in such a way, that it generates test scenarios accordingly.

System under test

Our example application is a tiny blog application. The next few test will target the persistence layer, which stores a blog post with comments.

Our tests will use @DataJpaTest, together with Spring runner, to create test a database and inject a repository.

Trivial example

The model

Our model has two states and three transitions.

In diagram form

 

Transition From state To state
addPost notPresent Present
removePost Present notPresent
updatePost Present Present

In table form

        // States
        final Vertex notPresent = new Vertex().setName("notPresent");
        final Vertex present = new Vertex().setName("present");
        // Transitions
        final Model model = new Model() //
                .addEdge(new Edge().setName("addPost") //
                        .setSourceVertex(notPresent)//
                        .setTargetVertex(present))
                .addEdge(new Edge().setName("removePost") //
                        .setSourceVertex(present) //
                        .setTargetVertex(notPresent)) //
                .addEdge(new Edge().setName("updatePost") //
                        .setSourceVertex(present) //
                        .setTargetVertex(present));

The model represented in code

 

Now that we have our model, we need to tell GraphWalker what to do with it. For example, we have to tell it where to start.

setNextElement(notPresent);

Next, we will tell GraphWalker to generate random paths.

setPathGenerator(new RandomPath(new TimeDuration(2, TimeUnit.SECONDS)));

GraphWalker can generate random paths indefinitely. So, we have to tell it when to stop. We have. among others,  the following options to do so:

  • EdgeCoverage stop when a certain coverage degree of edges (transitions) is walked
  • VertexCoverage stop when a certain coverage degree of vertices (states) has been visited
  • TimeDuration stop when a certain amount of time has passed

Gluing the model to our code

You have noticed we named all the vertices and edges and you have noted the names look like method names. They are method names. Upon traversing edge, the method it is named after gets executed. Upon entering a vertex, the method it is named after gets executed. These method provide the glue between our model and our code. The code executed at a vertex may contain assertions, do validate some results.

   public void notPresent() {
        assertFalse(repository.findByTitle(post.getTitle()).isPresent());
    }
    
    public void present() {
        assertTrue(repository.findByTitle(post.getTitle()).isPresent());
    }

The code executed at the edges applies the state changes implied by the transition.

   public void addPost() {
        repository.save(post);
    }
    
    public void updatePost() {
        repository.save(post);
    }
    
    public void removePost() {
        repository.removePost(post);
    }

Full source code

This example is trivial, just to show the basics. However, to test more complex software, using just states and transitions will not be enough.

Actions and guards

Guards further constrain which edges can be traversed. Without guards, any edge starting from a certain state may be chosen when the model is in that state. Guards add the ability to dynamically disable and enable some edges. Actions are executed upon walking an edge.

Both actions and guards, are specified as fragments of javascript. Guards must return a boolean, in contrast actions have no such constraints.

In our next example, we will add and remove comments. To make the test more accurate, we will add multiple comments to a post. We will also remove the comments, but only if there is at least one comment to remove.

The updatePost edge will be replaced by two new edges, addComment and removeComment.

       final Model model = new Model() //
                .addEdge(new Edge().setName("addPost") //
                        .addAction(new Action("var comments = 0"))
                        .setSourceVertex(notPresent)//
                        .setTargetVertex(present))
                .addEdge(new Edge().setName("removePost") //
                        .setSourceVertex(present) //
                        .setTargetVertex(notPresent)) //
                .addEdge(new Edge().setName("addComment") //
                        .addAction(new Action("comments++"))
                        .setSourceVertex(present) //
                        .setTargetVertex(present)) //
                .addEdge(new Edge().setName("removeComment") //
                        .setGuard(new Guard("comments > 0"))
                        .addAction(new Action("comments--"))
                        .setSourceVertex(present) //
                .setTargetVertex(present));

Full source code

As a post is added, var comments = 0 is executed. Then, as comments are added, comments++ is executed. The edge removeComment has both a guard and an action. The guard comments > 0 make sure the path will not be traversed if there are no comments to remove. Finally, the action comments-- updates the number of comments if the edge is traversed.

The addition of the number of comments makes our model an Extended Finite State Machine.

Conclusion

With GraphWalker we can use finite state machines to generate scenario’s, to test software that has state. Furthermore, using Javascript executed by GraphWalker, we can make these scenario’s more complete.