ModelJUnit tutorial

In this tutorial, we implemented the model created last time (see Modeling tutorial) using ModelJUnit.

The Model

First we implement the FsmModel interface to define our eFSM in ModelJUnit:

For every input in our model, we are going to write a ModelJUnit Action:

@Action
public void myInput() {
  // Here we should update the current state...
}

and a guard that’ll tell ModelJUnit when this action is available. The guard should be a method with the name <input>Guard and return a boolean, e.g.:

public boolean myInputGuard() {
  return /* true if the current state allows myInput, false otherwise */
}

Here is the model implemented during the tutorial

import foo.Model;
import nz.ac.waikato.modeljunit.Action;
import nz.ac.waikato.modeljunit.AllRoundTester;
import nz.ac.waikato.modeljunit.FsmModel;
import nz.ac.waikato.modeljunit.GreedyTester;
import nz.ac.waikato.modeljunit.LookaheadTester;
import nz.ac.waikato.modeljunit.RandomTester;
import nz.ac.waikato.modeljunit.Tester;
import nz.ac.waikato.modeljunit.VerboseListener;
import nz.ac.waikato.modeljunit.coverage.ActionCoverage;
import nz.ac.waikato.modeljunit.coverage.StateCoverage;
import nz.ac.waikato.modeljunit.coverage.TransitionCoverage;
import nz.ac.waikato.modeljunit.gui.visualisaton.VisualisationListener;


public class MyModel implements FsmModel {

    public enum Page { Login, Home, Lab }
    public enum LabStatus { Open, Submitted }

    private Page mPage = Page.Home;
    private LabStatus mStatus = LabStatus.Open ;
    private int nbFiles = 0;

    private MyAdapter mAdapter = new MyAdapter();

    @Action
    public void login() {
        mAdapter.login();
        mPage = Page.Home;
    }
    public boolean loginGuard() {
        return mPage == Page.Login;
    }

    @Action
    public void logout() {
        mAdapter.logout();
        mPage = Page.Login;
    }
    public boolean logoutGuard() {
        return mPage != Page.Login;
    }

    @Action
    public void clickLab() {
        mAdapter.clickLab();
        mPage = Page.Lab;
    }
    public boolean clickLabGuard() {
        return mPage == Page.Home;
    }

    @Action
    public void submit() {
        mAdapter.submit();
        mStatus = LabStatus.Submitted;
    }
    public boolean submitGuard() {
        return mPage == Page.Lab & mStatus == LabStatus.Open;
    }

    @Action
    public void withdraw() {
        mAdapter.withdraw();
        mStatus = LabStatus.Open;
    }
    public boolean withdrawGuard() {
        return mPage == Page.Lab & mStatus == LabStatus.Submitted;
    }

    @Override
    public Object getState() {
        return mPage + " " + mStatus;
    }

    @Override
    public void reset(boolean arg0) {
        mPage = Page.Login;
        if (arg0)   mAdapter.reset();

    }

    public static void main(String[] argv) {
        MyModel model = new MyModel();
        //Tester tester = new LookaheadTester(model);
        RandomTester tester = new RandomTester(model);
        //Tester tester = new AllRoundTester(model);
        //Tester tester = new GreedyTester(model);
        tester.buildGraph();
        tester.addListener(new VerboseListener());
        tester.addListener(new StopOnFailureListener());
        tester.addCoverageMetric(new TransitionCoverage());
        tester.addCoverageMetric(new StateCoverage());
        tester.addCoverageMetric(new ActionCoverage());
        tester.generate(20);
        tester.printCoverage();
    }
}

The Adapter

Next we implemented the adapter that allows our model to communicate with and take control of our SUT.

For each one of the @Action method in the model, we added a similarly named method in the adapter class:

class MyAdapter [
  // ...
  public void myInput() {
    // Here we execute myInput on the SUT
  }
  // ...

and, in the model, we make sure that each @Action calls the right adapter method:

class MyModel implements FsmModel {
 // ...
 @Action
 public void myInput() {
   // ...
   adapter.myInput();
 }

Here is the adapter created during the tutorial:

import java.util.concurrent.TimeUnit;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

import static org.junit.Assert.*;

public class MyAdapter {

    WebDriver mDriver;

    public MyAdapter() {
        mDriver = new FirefoxDriver();
        mDriver.manage().timeouts().implicitlyWait(3, TimeUnit.SECONDS);
        mDriver.get("http://localhost:5000/lbs/login");
    }

    public void reset() {
      mDriver.quit();
      mDriver = new FirefoxDriver();
      mDriver.get("http://localhost:5000/lbs/login");
      // Reset the fire database...
    }

    public void login() {
      mDriver.findElement(By.name("email")).sendKeys("arnarbi@gmail.com");
      mDriver.findElement(By.name("password")).sendKeys("test");
      mDriver.findElement(By.id("loginbtn")).click();
      assertEquals(
        "Laboration overview for Student Birgisson (TDA602)",
        mDriver.findElement(By.tagName("h1")).getText()
      );
    }

    public void logout() {
      mDriver.findElement(By.cssSelector("a[href='/lbs/logout']")).click();
      assertEquals(
          "Please log in",
          mDriver.findElement(By.tagName("h1")).getText()
      );
    }

    public void clickLab() {
      mDriver.findElement(By.linkText("Project report")).click();
      assertEquals(
          "Lab details for Project report",
          mDriver.findElement(By.tagName("h1")).getText()
      );
    }

    public void submit() {
      mDriver.findElement(By.id("submit-button")).click();
      mDriver.findElement(By.id("confirm-submit-button")).click();
      // check that the withdraw button is now present
      mDriver.findElement(By.name("withdraw"));
    }

    public void withdraw() {
      mDriver.findElement(By.name("withdraw")).click();
      // check that the submit button is now present
      mDriver.findElement(By.id("submit-button"));
    }
}

Implementing reset

One of the methods that you need to implement as part of the FsmModel interface is reset. In this method, your model is supposed to reset itself as well as reset the SUT to a known state.

In case your SUT only store data in the browser, you can simply close the browser and open a new instance. But, in case your sut has a server-side data storage (e.g. a database) you will need to reset this to a known state as well. This might mean overriding files, clearing a mysql table or running a command.

Implementing getState

The second method that the FsmModel interface requires you to implement is getState. This method should return an object, often a string, which uniquely identify the current state. Unlike what I did during the tutorial, do not include all the state variables in the returned stat, instead it should correspond to the Visible States of your model (the states in the bubbles).

Generating tests

Once you have implemented your model, you can use it to generate tests. In the tutorial example, tests were generated in the main method:

public static void main(String[] argv) {
    MyModel model = new MyModel();
    //Tester tester = new LookaheadTester(model);
    RandomTester tester = new RandomTester(model);
    //Tester tester = new AllRoundTester(model);
    //Tester tester = new GreedyTester(model);
    tester.buildGraph();
    tester.addListener(new VerboseListener());
    tester.addListener(new StopOnFailureListener());
    tester.addCoverageMetric(new TransitionCoverage());
    tester.addCoverageMetric(new StateCoverage());
    tester.addCoverageMetric(new ActionCoverage());
    tester.generate(20);
    tester.printCoverage();
}

First, we need to instantiate your model:

MyModel model = new MyModel();

Then, we choose the test strategy. ModelJUnit offers four different strategies: AllRoundTester, GreedyTester, LookaheadTester and RandomTester:

RandomTester tester = new RandomTester(model);

Next we tell the tester to try and build the graph from the model implementation. This will basically generate a lot of tests until ModelJUnit thinks it has seen all the states. It’ll be used later to compute the coverage metrics. (Hint: if you want the graph building to go faster, you can write your model so that it is possible to connect the adapter after you have run buildGraph):

tester.buildGraph();

Then, we tell ModelJUnit to collect some information about the test run:

tester.addListener(new VerboseListener());
tester.addCoverageMetric(new TransitionCoverage());
tester.addCoverageMetric(new StateCoverage());
tester.addCoverageMetric(new ActionCoverage());

Caution

There is a bug in ModelJUnit that prevent you to get correct state coverage with the AllRoundTester. The coverage you get is only the number of covered states since the last reset.

To get around this problem, use the following code to get state coverage:

tester.addCoverageMetric(new StateCoverage() {
    @Override
    public String getName() {
        return "Total state coverage";
    }
});

This tells ModelJUnit to stop the test on the first failure:

tester.addListener(new StopOnFailureListener());

Final, we generate a test sequence with a given length and print the collected metrics:

tester.generate(20);
tester.printCoverage();