Build a sample solution to Assignment 1, using TDD techniques, Maven & Eclipse
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
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] ------------------------------------------------------------------------
We can now import this into eclipse. Launch Eclipse and select File->Import->Maven->Existing Maven Projects
The project workspace should look like this:
To get an overview of the proposed solution, have a look at the below architecture diagrams (produced using Structure101 software).
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.
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.
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).
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.
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
In src/main/java
source folder in Eclipse, create a new package called models
. In this package create a new class called 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:
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):
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")));
}
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] ------------------------------------------------------------------------
Introduce the following models:
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);
}
}
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);
}
}
In src/test/java
we can bring in our tests
First the 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)));
}
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());
}
}
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:
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:
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());
}
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:
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:
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.
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());
}
}
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.
Archive of the project so far can be found here.
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] ------------------------------------------------------------------------
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:
<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>
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).