Objectives

Build a sample solution to Assignment 1, using TDD techniques, Maven & Eclipse

Setup: Maven

Before commencing this lab you must have Maven installed successfully. To verify this, open a command line and enter this command:

mvn -version

It should report something like this:

Maven home: /Users/edeleastar/dev/apache-maven-3.5.0
Java version: 1.8.0_60, vendor: Oracle Corporation
Java home: /Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/jre
Default locale: en_IE, platform encoding: UTF-8
OS name: "mac os x", version: "10.12.6", arch: "x86_64", family: "mac"

(The operation system may be different).

Manually create a new blank folder called pacemaker-console-solution.

Within this folder, create the following directory structure:

pacemaker-console-solution
     │
     └── src
           │
           ├── main
           │     │ 
           │     └──java
           │
           └── test
                 │
                 └──java

pom.xml

Using any text editor, in the root of the folder, create the pom.xml file:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>pacemaker</groupId>
  <artifactId>pacemaker-console-solution</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>pacemaker-console-solution</name>
  <url>https://wit-computing-msc-2017.github.io/agile</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <version>3.7.0</version>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
      </plugin>
    </plugins>
  </build>

  <dependencies>
  </dependencies>
</project>

This is a minimal starter POM for a standard java application. To verify that we have a valid POM, run the following command from within the project folder:

mvn verify

This should report BUILD SUCCESS (note you many have warnings appearing before this output...that's ok):

[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building pacemaker-console-solution 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.145 s
[INFO] Finished at: 2017-10-19T14:40:15+01:00
[INFO] Final Memory: 5M/123M
[INFO] ------------------------------------------------------------------------

Setup : Eclipse

We can now import this into eclipse. Launch Eclipse and select File->Import->Maven->Existing Maven Projects

The project workspace should look like this:

Architecture

To get an overview of the proposed solution, have a look at the below architecture diagrams (produced using Structure101 software).

Packages and their entities

The assignment solution has a number of source files and interdependencies between these files.

We use containers (packages) to manage complexity. Containment creates dependencies between containers.

Dependencies

The dependencies in our assignment solution:

When dependencies all point in the same direction (e.g. downwards), the cumulative dependency is smallest.

Another view of our dependencies, with cardinalities representing the number of calls. This image should give you an appreciation of how the source files interact in our system.

Classes/Interfaces and their responsibilities

controllers:

  • Main: Starts the app. Creates a console shell attached to the PacemakerConsoleService. Automatically saves the data created in the session upon exit.

  • PacemakerConsoleService: defines the shell commands and invokes relevant Console command to render the data (coming from PacemakerAPI) on the console.

  • PacemakerAPI: The pacemaker API that manages the user, activity and location data in the system (note: it is now independent of I/O approach).

models:

  • User: manages a single user and their associated activities

  • Activity: manages a single activity and the routes (Location) associated with the activity.

  • Location: manages a single location.

utils:

  • Console: A utility that acts as a base class for rendering users and activities onto the console. Basic System.out.println is used in this base class. Such as approach allows for future app evolutions to use other parsers.

  • AsciiTableParser: A utility that extends the Console class and overrides the methods to render users and activities on the console using the ASCIITable utility. Also provides an status (e.g ok, not found) when rendering data.

  • TimeFormatters: A utility that provides methods to parseDateTime and parseDuration. These methods are used mainly in the Activity class.

  • Serializer: an interface to provide a generic set of method signatures for serialization.

  • JSONSerializer: manages the input and output (serialization) of JSON objects, adhering to the Serializer interface.

  • XMLSerializer: manages the input and output (serialization) of overrides the objects, adhering to the Serializer interface

User Model

In src/main/java source folder in Eclipse, create a new package called models. In this package create a new class called User:

User

package models;

import static com.google.common.base.MoreObjects.toStringHelper;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import com.google.common.base.Objects;

public class User implements Serializable {

  public String id;
  public String firstName;
  public String lastName;
  public String email;
  public String password;

  public User() {
  }

  public String getId() {
    return id;
  }

  public String getFirstname() {
    return firstName;
  }

  public String getLastname() {
    return lastName;
  }

  public String getEmail() {
    return email;
  }

  public User(String firstName, String lastName, String email, String password) {
    this.id = UUID.randomUUID().toString();
    this.firstName = firstName;
    this.lastName = lastName;
    this.email = email;
    this.password = password;
  }

  @Override
  public boolean equals(final Object obj) {
    if (obj instanceof User) {
      final User other = (User) obj;
      return Objects.equal(firstName, other.firstName)
          && Objects.equal(lastName, other.lastName)
          && Objects.equal(email, other.email)
          && Objects.equal(password, other.password);
    } else {
      return false;
    }
  }

  @Override
  public String toString() {
    return toStringHelper(this).addValue(id)
        .addValue(firstName)
        .addValue(lastName)
        .addValue(password)
        .addValue(email)
        .toString();
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(this.id, this.lastName, this.firstName, this.email, this.password);
  }
}

This will have errors immediately - as we are relying on the guava library, currently not included in the dependencies. Insert the following in the the <dependencies> in the POM:

    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>23.0</version>
    </dependency>

Eclipse should trigger maven to download and include the library in the project, eliminating the errors. Notice that the Eclipse workspace will include a new Maven Dependencies entry:

The new dependencies include other libraries upstream of the guava library. These can also be seen from the Maven Central repository for guava:

User Unit Test

In the src/test/java folder, create a models package. Bring in these 2 classes (if prompted to add JUnit to the build path, don't...we will add it via pom.xml):

Fixtures

package models;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Fixtures {

  public static List<User> users = new ArrayList<>(Arrays.asList(
      new User("marge", "simpson", "marge@simpson.com", "secret"),
      new User("lisa", "simpson", "lisa@simpson.com", "secret"),
      new User("bart", "simpson", "bart@simpson.com", "secret"),
      new User("maggie", "simpson", "maggie@simpson.com", "secret")));
}

UserTest

package models;

import static org.junit.Assert.*;

import java.util.HashSet;
import java.util.Set;

import org.junit.Test;

import static models.Fixtures.users;

public class UserTest {

  User homer = new User("homer", "simpson", "homer@simpson.com", "secret");

  @Test
  public void testCreate() {
    assertEquals("homer", homer.firstName);
    assertEquals("simpson", homer.lastName);
    assertEquals("homer@simpson.com", homer.email);
    assertEquals("secret", homer.password);
  }

  @Test
  public void testIds() {
    Set<String> ids = new HashSet<>();
    for (User user : users) {
      ids.add(user.id);
    }
    assertEquals(users.size(), ids.size());
  }

  @Test
  public void testToString() {
    assertEquals("User{" + homer.id + ", homer, simpson, secret, homer@simpson.com}",
        homer.toString());
  }
}

The test will have syntax errors - again called by missing dependencies. Here is the JUnit library we are missing:

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>

The project should have no errors now - and we should be able to run the unit test we have just introduced:

We can also run these tests independently of Eclipse on the command line:

$ mvn test

...

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running models.UserTest
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.124 sec

Results :

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.952 s
[INFO] Finished at: 2017-10-16T09:07:52+01:00
[INFO] Final Memory: 15M/208M
[INFO] ------------------------------------------------------------------------

Activity & Location Models

Introduce the following models:

Activity

package models;

import static com.google.common.base.MoreObjects.toStringHelper;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import com.google.common.base.Objects;

public class Activity implements Serializable {

  public String id;
  public String type;
  public String location;
  public double distance;
  public List<Location> route = new ArrayList<>();

  public Activity() {
  }

  public Activity(String type, String location, double distance, String start, String duration) {
    this.id = UUID.randomUUID().toString();
    this.type = type;
    this.location = location;
    this.distance = distance;
  }

  public String getId() {
    return id;
  }

  public String getType() {
    return type;
  }

  public String getLocation() {
    return location;
  }

  public String getDistance() {
    return Double.toString(distance);
  }

  public String getRoute() {
    return route.toString();
  }

  @Override
  public boolean equals(final Object obj) {
    if (obj instanceof Activity) {
      final Activity other = (Activity) obj;
      return Objects.equal(type, other.type)
          && Objects.equal(location, other.location)
          && Objects.equal(distance, other.distance)
          && Objects.equal(route, other.route);
    } else {
      return false;
    }
  }

  @Override
  public String toString() {
    return toStringHelper(this).addValue(id)
        .addValue(type)
        .addValue(location)
        .addValue(distance)
        .addValue(route)
        .toString();
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(this.id, this.type, this.location, this.distance);
  }
}

Location

package models;

import static com.google.common.base.MoreObjects.toStringHelper;

import java.io.Serializable;
import java.util.UUID;

import com.google.common.base.Objects;

public class Location implements Serializable {

  public String id;
  public double longitude;
  public double latitude;

  public Location() {
  }

  public Location(double latitude, double longitude) {
    this.id = UUID.randomUUID().toString();
    this.latitude = latitude;
    this.longitude = longitude;
  }


  @Override
  public boolean equals(final Object obj) {
    if (obj instanceof Location) {
      final Location other = (Location) obj;
      return Objects.equal(latitude, other.latitude)
          && Objects.equal(longitude, other.longitude);
    } else {
      return false;
    }
  }

  @Override
  public String toString() {
    return toStringHelper(this).addValue(id)
        .addValue(latitude)
        .addValue(longitude)
        .toString();
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(this.id, this.latitude, this.longitude);
  }
}

Tests

In src/test/java we can bring in our tests

First the Fixtures:

Fixtures

package models;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Fixtures {

  public static List<User> users = new ArrayList<>(Arrays.asList(
      new User("marge", "simpson", "marge@simpson.com", "secret"),
      new User("lisa", "simpson", "lisa@simpson.com", "secret"),
      new User("bart", "simpson", "bart@simpson.com", "secret"),
      new User("maggie", "simpson", "maggie@simpson.com", "secret")));

  public static List<Activity> activities = new ArrayList<>(Arrays.asList(
        new Activity("walk", "fridge", 0.001, "10:9:2017 09:00:00", "00:42:20"),
        new Activity("walk", "bar", 1.0, "11:9:2017 10:00:00", "00:39:02"),
        new Activity("run", "work", 2.2, "12:9:2017 08:00:00", "00:54:23"),
        new Activity("walk", "shop", 2.5, "13:9:2017 10:00:00", "00:32:03"),
        new Activity("cycle", "school", 4.5, "14:9:2017 11:00:00", "00:47:04")));

    public static List<Location> locations = new ArrayList<>(Arrays.asList(
        new Location(23.3, 33.3),
        new Location(34.4, 45.2),
        new Location(25.3, 34.3),
        new Location(44.4, 23.3)));
}

LocationTest

package models;

import static org.junit.Assert.*;

import org.junit.Test;

import static models.Fixtures.locations;

public class LocationTest {

  @Test
  public void testCreate() {
    assertEquals(0.01, 23.3, locations.get(0).latitude);
    assertEquals(0.01, 33.3, locations.get(0).longitude);
  }

  @Test
  public void testIds() {
    assertNotEquals(locations.get(0).id, locations.get(1).id);
  }

  @Test
  public void testToString() {
    assertEquals("Location{" + locations.get(0).id + ", 23.3, 33.3}", locations.get(0).toString());
  }
}

ActivityTest

package models;

import static org.junit.Assert.*;
import java.util.HashSet;
import java.util.Set;
import org.junit.Test;
import static models.Fixtures.activities;

public class ActivityTest {
  Activity test = new Activity("walk", "fridge", 0.001, "11:2:2012 9:00:00", "20:00:00");

  @Test
  public void testCreate() {
    assertEquals("walk", test.type);
    assertEquals("fridge", test.location);
    assertEquals(0.0001, 0.001, test.distance);
  }

  @Test
  public void testIds() {
    Set<String> ids = new HashSet<>();
    for (Activity activity : activities) {
      ids.add(activity.id);
    }
    assertEquals(activities.size(), ids.size());
  }

  @Test
  public void testToString() {
    assertEquals("Activity{" + test.id + ", walk, fridge, 0.001, []}", test.toString());
  }
}

All tests should now pass:

Refining the Models

Currently User and Activity are independent classes. We would like to establish a one-to-many relationship. In this revised version, we establish the relationship:

User

package models;

import static com.google.common.base.MoreObjects.toStringHelper;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import com.google.common.base.Objects;

public class User implements Serializable {

  public String id;
  public String firstName;
  public String lastName;
  public String email;
  public String password;

  public Map<String, Activity> activities = new HashMap<>();

  public User() {
  }

  public String getId() {
    return id;
  }

  public String getFirstname() {
    return firstName;
  }

  public String getLastname() {
    return lastName;
  }

  public String getEmail() {
    return email;
  }

  public User(String firstName, String lastName, String email, String password) {
    this.id = UUID.randomUUID().toString();
    this.firstName = firstName;
    this.lastName = lastName;
    this.email = email;
    this.password = password;
  }

  @Override
  public boolean equals(final Object obj) {
    if (obj instanceof User) {
      final User other = (User) obj;
      return Objects.equal(firstName, other.firstName)
          && Objects.equal(lastName, other.lastName)
          && Objects.equal(email, other.email)
          && Objects.equal(password, other.password)
          && Objects.equal(activities, other.activities);
    } else {
      return false;
    }
  }

  @Override
  public String toString() {
    return toStringHelper(this).addValue(id)
        .addValue(firstName)
        .addValue(lastName)
        .addValue(password)
        .addValue(email)
        .addValue(activities)
        .toString();
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(this.id, this.lastName, this.firstName, this.email, this.password, this.activities);
  }
}

This version causes one of our tests to fail:

The failure is in this test here:

  @Test
  public void testToString() {
    assertEquals("User{" + homer.id + ", homer, simpson, secret, homer@simpson.com",
        homer.toString());
  }

which doesnt take account of the relationship to Activities. This is a small update to make the tests pass again:

  @Test
  public void testToString() {
    assertEquals("User{" + homer.id + ", homer, simpson, secret, homer@simpson.com, {}}",
        homer.toString());
  }

We can introduce an additional test to exercise the User->Activity relationship:

  @Test
  public void tesAddActivity() {
    Activity activity = new Activity("walk", "fridge", 0.001, "11:2:2012 9:00:00", "20:00:00");
    homer.activities.put(activity.id,activity);
    System.out.println(homer);
    assertEquals("User{" + homer.id + ", homer, simpson, secret, homer@simpson.com, {" + activity.id + "=Activity{" + activity.id + ", walk, fridge, 0.001, []}}}",
        homer.toString());
  }

Activity Time & Duration Support

Parsing date, times and durations can be a challenge - with various libraries, formatters and technical approaches. The Joda time library is a well respected approach:

Introduce this class to encapsulate the transformation of strings to/from dates/times and durations:

utils/TimeFormatters

package utils;

import org.joda.time.format.PeriodFormatterBuilder;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.PeriodFormatter;

import org.joda.time.DateTime;
import org.joda.time.Duration;

public class TimeFormatters {

  static PeriodFormatter periodFormatter = new PeriodFormatterBuilder().printZeroAlways()
      .appendHours()
      .appendSeparator(":")
      .appendMinutes()
      .appendSeparator(":")
      .appendSeconds()
      .toFormatter();

  static DateTimeFormatter dateFormatter = DateTimeFormat.forPattern("dd:MM:yyyy HH:mm:ss");

  public static DateTime parseDateTime(String dateTime) {
    return new DateTime(dateFormatter.parseDateTime(dateTime));
  }

  public static String parseDateTime(DateTime dateTime) {
    return dateFormatter.print(dateTime);
  }

  public static Duration parseDuration(String duration) {
    return periodFormatter.parsePeriod(duration).toStandardDuration();
  }

  public static String parseDuration(Duration duration) {
    return periodFormatter.print(duration.toPeriod());
  }
}

This four static methods provide the basic transformations we need.

Introduce the joda-time maven dependency...locate it yourself from the maven repo:

The Activity class can now be completed:

Activity.java

package models;

import static com.google.common.base.MoreObjects.toStringHelper;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import com.google.common.base.Objects;
import org.joda.time.DateTime;
import org.joda.time.Duration;

import static utils.TimeFormatters.parseDateTime;
import static utils.TimeFormatters.parseDuration;

public class Activity implements Serializable {

  public String id;
  public String type;
  public String location;
  public double distance;
  public DateTime starttime;
  public Duration duration;
  public List<Location> route = new ArrayList<>();

  public Activity() {
  }

  public Activity(String type, String location, double distance, String start, String duration) {
    this.id = UUID.randomUUID().toString();
    this.type = type;
    this.location = location;
    this.distance = distance;
    this.starttime = parseDateTime(start);
    this.duration = parseDuration(duration);
  }

  public String getId() {
    return id;
  }

  public String getType() {
    return type;
  }

  public String getLocation() {
    return location;
  }

  public String getDistance() {
    return Double.toString(distance);
  }

  public String getRoute() {
    return route.toString();
  }

  @Override
  public boolean equals(final Object obj) {
    if (obj instanceof Activity) {
      final Activity other = (Activity) obj;
      return Objects.equal(type, other.type)
          && Objects.equal(location, other.location)
          && Objects.equal(distance, other.distance)
          && Objects.equal(starttime, other.starttime)
          && Objects.equal(duration, other.duration)
          && Objects.equal(route, other.route);
    } else {
      return false;
    }
  }

  @Override
  public String toString() {
    return toStringHelper(this).addValue(id)
        .addValue(type)
        .addValue(location)
        .addValue(distance)
        .addValue(parseDateTime(starttime))
        .addValue(parseDuration(duration))  
        .addValue(route)
        .toString();
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(this.id, this.type, this.location, this.distance, this.starttime, this.duration);
  }
}

Several tests will now fail, as our toString methods are now reporting the time/duration values.

ActivityTest

package models;

import static org.junit.Assert.*;
import java.util.HashSet;
import java.util.Set;
import org.junit.Test;
import static models.Fixtures.activities;

public class ActivityTest {
  Activity test = new Activity("walk", "fridge", 0.001, "11:02:2012 9:00:00", "20:0:0");

  @Test
  public void testCreate() {
    assertEquals("walk", test.type);
    assertEquals("fridge", test.location);
    assertEquals(0.0001, 0.001, test.distance);
  }

  @Test
  public void testIds() {
    Set<String> ids = new HashSet<>();
    for (Activity activity : activities) {
      ids.add(activity.id);
    }
    assertEquals(activities.size(), ids.size());
  }

  @Test
  public void testToString() {
    assertEquals("Activity{" + test.id + ", walk, fridge, 0.001, 11:02:2012 09:00:00, 20:0:0, []}", test.toString());
  }
}

UserTest

package models;

import static org.junit.Assert.*;

import java.util.HashSet;
import java.util.Set;

import org.junit.Test;

import static models.Fixtures.users;

public class UserTest {

  User homer = new User("homer", "simpson", "homer@simpson.com", "secret");

  @Test
  public void testCreate() {
    assertEquals("homer", homer.firstName);
    assertEquals("simpson", homer.lastName);
    assertEquals("homer@simpson.com", homer.email);
    assertEquals("secret", homer.password);
  }

  @Test
  public void testIds() {
    Set<String> ids = new HashSet<>();
    for (User user : users) {
      ids.add(user.id);
    }
    assertEquals(users.size(), ids.size());
  }

  @Test
  public void testToString() {
    assertEquals("User{" + homer.id + ", homer, simpson, secret, homer@simpson.com, {}}",
        homer.toString());
  }


  @Test
  public void tesAddActivity() {
    Activity activity = new Activity("walk", "fridge", 0.001, "11:2:2012 9:00:00", "20:00:00");
    homer.activities.put(activity.id,activity);
    assertEquals("User{" + homer.id + ", homer, simpson, secret, homer@simpson.com, {" + activity.id + "=Activity{" + activity.id + ", walk, fridge, 0.001, 11:02:2012 09:00:00, 20:0:0, []}}}",
        homer.toString());
  }
}

Run all tests again now.

Exercises

Archive of the project so far can be found here.

Exercise 1: Maven test

Run the tests using the mvn test command. Verify that all tests pass, as is shown in the following output:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running models.ActivityTest
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.279 sec
Running models.LocationTest
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 sec
Running models.UserTest
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0 sec

Results :

Tests run: 10, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 8.360 s
[INFO] Finished at: 2017-10-20T12:03:28+01:00
[INFO] Final Memory: 15M/125M
[INFO] ------------------------------------------------------------------------

Exercise 2: Maven Coverage Reports

In Eclipse, select Run, followed by Coverage....

When the Coverage Configurations window appears, click on the Coverage tab and confine the analysis to the src/main/java folder, as is shown below:

When you click the Coverage button, the JUnit tests should run and a coverage analysis presented to you:

This report clearly shows that our models need to have more JUnit tests written to bring up the percentage from the low 30's to a recommended coverage of 80%.

Try adding in more tests and watch the % rise.

Note that running analysis on the coverage % does not generate any coverage reports to the target directory:

A good video on Eclipse Oxygen and Code Coverage in Practice (plus the history of code coverage):

You can export the coverage analyis:

  • manually: File, Export..., Run/Debug, Coverage Session.
  • via maven to the target directory: using the plugin below and running the mvn jacoco:report command.
                <plugin>
                    <groupId>org.jacoco</groupId>
                    <artifactId>jacoco-maven-plugin</artifactId>
                    <version>0.7.9</version>                      
                    <executions>
                        <execution>
                            <id>default-prepare-agent</id>
                            <goals><goal>prepare-agent</goal></goals>
                        </execution>
                        <execution>
                            <id>default-report</id>
                            <phase>prepare-package</phase>
                            <goals><goal>report</goal></goals>
                        </execution>
                    </executions>
                </plugin>

The next lab...

With a set of models + unit tests + initial fixtures in place, we have a solid foundation for proceeding to implement the core API + console user interface (next lab).