Equip Pacemaker with JUnit libraries and then introduce a range of tests to verify essential features. Correct issues that arise as a result of the tests, becoming familiar with the fail/pass/refactor/pass cycle. Review the outstanding command set of the application. We will use JUnit 4 in this lab.
We will introduce unit testing infrastructure + unit tests to exercise the features of the app developed so far.
First, locate the project properties (context menu->Build Path->Configure Build Path):
Select Libraries -> Add Library:
And select JUnit - make sure it is JUnit 4. The dependency should be clear from the project workspace:
Now create a new 'source' folder in the project called 'test'. Once created, replicate the package structure in this folder. You workspace should look like this:
The new packages are empty for the moment.
Create a new unit test in the new models package:
Call the test "LocationTest"
The generated class should look like this:
package models;
import static org.junit.Assert.*;
import org.junit.Test;
public class LocationTest
{
@Test
public void test()
{
fail("Not yet implemented");
}
}
To run the test, select the 'test' folder, right click and select 'Run As->Junit Test'
This should display the JUnit Test Runner:
This is our first unit test - deliberately failing for the moment.
Replace the failing test with the following:
@Test
public void testCreate()
{
Location one = new Location(23.3f, 33.3f);
assertEquals (0.01, 23.3f, one.latitude);
assertEquals (0.01, 33.3f, one.longitude);
}
Which should now pass:
Some more tests:
@Test
public void testIds()
{
Location one = new Location(23.3f, 33.3f);
Location two = new Location(34.4f, 22.2f);
assertNotEquals(one.id, two.id);
}
@Test
public void testToString()
{
Location one = new Location(23.3f, 33.3f);
assertEquals ("Location{2, 23.3, 33.3}", one.toString());
}
All should now be passing:
If your tests are failing, revisit your Location.java code and make changes to it so that the tests pass; avoid the temptation to change the tests to suit your code!
We can simplify the test marginally by sharing the object initializations between tests:
package models;
import static org.junit.Assert.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class LocationTest
{
private Location one;
private Location two;
@Before
public void setup()
{
one = new Location(23.3f, 33.3f);
two = new Location(34.4f, 22.2f);
}
@After
public void tearDown()
{
one = two = null;
}
@Test
public void testCreate()
{
assertEquals (0.01, 23.3, one.latitude);
assertEquals (0.01, 33.3, one.longitude);
}
@Test
public void testIds()
{
assertNotEquals(one.id, two.id);
}
@Test
public void testToString()
{
assertEquals ("Location{2, 23.3, 33.3}", one.toString());
}
}
Ready this carefully - the tests are identical, with the setup/tearDown methods called before/after each individual test.
Include this test:
package models;
import static org.junit.Assert.*;
import java.util.HashSet;
import java.util.Set;
import org.junit.Test;
public class UserTest
{
private User[] users =
{
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")
};
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<Long> ids = new HashSet<>();
for (User user : users)
{
ids.add(user.id);
}
assertEquals (users.length, ids.size());
}
@Test
public void testToString()
{
assertEquals ("User{" + homer.id + ", homer, simpson, secret, homer@simpson.com, {}}", homer.toString());
}
}
Make sure it passes. Note carefully the testIds()
test. Can you see its rationale?
Here is an ActivityTest:
package models;
import static org.junit.Assert.*;
import java.util.HashSet;
import java.util.Set;
import org.junit.Test;
public class ActivityTest
{
private Activity[] activities =
{
new Activity ("walk", "fridge", 0.001),
new Activity ("walk", "bar", 1.0),
new Activity ("run", "work", 2.2),
new Activity ("walk", "shop", 2.5),
new Activity ("cycle", "school", 4.5)
};
Activity test = new Activity ("walk", "fridge", 0.001);
@Test
public void testCreate()
{
assertEquals ("walk", test.type);
assertEquals ("fridge", test.location);
assertEquals (0.0001, 0.001, test.distance);
}
@Test
public void testIds()
{
Set<Long> ids = new HashSet<>();
for (Activity activity : activities)
{
ids.add(activity.id);
}
assertEquals (activities.length, ids.size());
}
@Test
public void testToString()
{
assertEquals ("Activity{" + test.id + ", walk, fridge, 0.001, []}", test.toString());
}
}
All these tests should pass, and should be able to run all test in one test runner:
In preparation for testing the PacemakerAPI, it may be useful to integrate all the static fixture data into a single class:
package models;
public class Fixtures
{
public static User[] users =
{
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 Activity[] activities =
{
new Activity ("walk", "fridge", 0.001),
new Activity ("walk", "bar", 1.0),
new Activity ("run", "work", 2.2),
new Activity ("walk", "shop", 2.5),
new Activity ("cycle", "school", 4.5)
};
public static Location[]locations =
{
new Location(23.3f, 33.3f),
new Location(34.4f, 45.2f),
new Location(25.3f, 34.3f),
new Location(44.4f, 23.3f)
};
}
This will enable us to simplify the three unit test somewhat:
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[0].latitude);
assertEquals (0.01, 33.3, locations[0].longitude);
}
@Test
public void testIds()
{
assertNotEquals(locations[0].id, locations[1].id);
}
@Test
public void testToString()
{
assertEquals ("Location{" + locations[0].id + ", 23.3, 33.3}", locations[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);
@Test
public void testCreate()
{
assertEquals ("walk", test.type);
assertEquals ("fridge", test.location);
assertEquals (0.0001, 0.001, test.distance);
}
@Test
public void testIds()
{
Set<Long> ids = new HashSet<>();
for (Activity activity : activities)
{
ids.add(activity.id);
}
assertEquals (activities.length, ids.size());
}
@Test
public void testToString()
{
assertEquals ("Activity{" + test.id + ", walk, fridge, 0.001, []}", 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<Long> ids = new HashSet<>();
for (User user : users)
{
ids.add(user.id);
}
assertEquals (users.length, ids.size());
}
@Test
public void testToString()
{
assertEquals ("User{" + homer.id + ", homer, simpson, secret, homer@simpson.com, {}}", homer.toString());
}
}
Integrate these version and verify that app passes all tests.
Commit these changes with a suitable message.
PacemakerAPI is a more sophisticated class, whose testing will require a little more discipline and focus. We can start with a skeleton, which will include static imports + a fixture:
package controllers;
import static org.junit.Assert.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import controllers.PacemakerAPI;
import static models.Fixtures.users;
import static models.Fixtures.activities;
import static models.Fixtures.locations;
public class PacemakerAPITest
{
private PacemakerAPI pacemaker;
@Before
public void setup()
{
pacemaker = new PacemakerAPI(null);
}
@After
public void tearDown()
{
pacemaker = null;
}
}
As there are no @Test methods, there are no test cases to run. Now introduce the following:
@Test
public void testUser()
{
assertEquals (0, pacemaker.getUsers().size());
}
This verifies that we are starting with an empty users datastore. We can augment the test:
@Test
public void testUser()
{
User homer = new User ("homer", "simpson", "homer@simpson.com", "secret");
assertEquals (0, pacemaker.getUsers().size());
pacemaker.createUser("homer", "simpson", "homer@simpson.com", "secret");
assertEquals (1, pacemaker.getUsers().size());
}
When we import the User class, this should also pass...
Our API returns a User object - which we can now verify:
@Test
public void testUser()
{
User homer = new User ("homer", "simpson", "homer@simpson.com", "secret");
assertEquals (0, pacemaker.getUsers().size());
pacemaker.createUser("homer", "simpson", "homer@simpson.com", "secret");
assertEquals (1, pacemaker.getUsers().size());
assertEquals (homer, pacemaker.getUserByEmail("homer@simpson.com"));
}
This time the test fails. The default behaviour of this test is to trigger a call to 'equals()' on the User objects. We haven't implemented this method, so the default behaviour is to do an identity comparison:
Implementing Equals can be tricky - and there are numerous approaches:
However, we are already using guava support for toString, so our easiest route is to use the equivalent for equals implementation. This means revisiting the User class, introducing this implementation:
@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;
}
}
We should verify our understanding of this method by introducing a new test into UserTest:
@Test
public void testEquals()
{
User homer = new User ("homer", "simpson", "homer@simpson.com", "secret");
User homer2 = new User ("homer", "simpson", "homer@simpson.com", "secret");
User bart = new User ("bart", "simpson", "bartr@simpson.com", "secret");
assertEquals(homer, homer);
assertEquals(homer, homer2);
assertNotEquals(homer, bart);
}
This should pass, and out PacemakerAPITest should also now pass.
Identity and Equality are important concepts to understand - and there are JUnit primitives that can be used to verify two characteristics individually. Extend the testEquals method as follows:
assertSame(homer, homer);
assertNotSame(homer, homer2);
This now verifies that homer and homer2 are equal (equality or equivalence test), but not the same (identity test).
Location and Activity should also have their own equals implementations:
@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 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;
}
}
What, precisely, will happen when this call is triggered in Activity.equals?
&& Objects.equal(route, other.route);
Introduce this unit test
@Test
public void testUsers()
{
for (User user : users)
{
pacemaker.createUser(user.firstName, user.lastName, user.email, user.password);
}
assertEquals (users.length, pacemaker.getUsers().size());
}
This test should succeed. However, we can strengthen the test now that we have equals methods in place:
@Test
public void testUsers()
{
for (User user : users)
{
pacemaker.createUser(user.firstName, user.lastName, user.email, user.password);
}
assertEquals (users.length, pacemaker.getUsers().size());
for (User user: users)
{
User eachUser = pacemaker.getUserByEmail(user.email);
assertEquals (user, eachUser);
assertNotSame(user, eachUser);
}
}
Now we can test delete:
@Test
public void testDeleteUsers()
{
for (User user : users)
{
pacemaker.createUser(user.firstName, user.lastName, user.email, user.password);
}
assertEquals (users.length, pacemaker.getUsers().size());
User marge = pacemaker.getUserByEmail("marge@simpson.com");
pacemaker.deleteUser(marge.id);
assertEquals (users.length-1, pacemaker.getUsers().size());
}
We should continually review our test code in an attempt to keep it as concise as possible. We can consider the test code as important as the main app, with the same care to remove repetition and generally keep in good shape.
All the tests in PacemakerAPITest can be simplified if we share a fixture:
public class PacemakerAPITest
{
private PacemakerAPI pacemaker;
@Before
public void setup()
{
pacemaker = new PacemakerAPI(null);
for (User user : users)
{
pacemaker.createUser(user.firstName, user.lastName, user.email, user.password);
}
}
@After
public void tearDown()
{
pacemaker = null;
}
@Test
public void testUser()
{
assertEquals (users.length, pacemaker.getUsers().size());
pacemaker.createUser("homer", "simpson", "homer@simpson.com", "secret");
assertEquals (users.length+1, pacemaker.getUsers().size());
assertEquals (users[0], pacemaker.getUserByEmail(users[0].email));
}
@Test
public void testUsers()
{
assertEquals (users.length, pacemaker.getUsers().size());
for (User user: users)
{
User eachUser = pacemaker.getUserByEmail(user.email);
assertEquals (user, eachUser);
assertNotSame(user, eachUser);
}
}
@Test
public void testDeleteUsers()
{
assertEquals (users.length, pacemaker.getUsers().size());
User marge = pacemaker.getUserByEmail("marge@simpson.com");
pacemaker.deleteUser(marge.id);
assertEquals (users.length-1, pacemaker.getUsers().size());
}
}
Note the adjustments made to testUser in this context.
These three methods in PacemakerAPI are untested:
public void createActivity(Long id, String type, String location, double distance)
public Activity getActivity (Long id)
public void addLocation (Long id, float latitude, float longitude)
The first one, in particular, seems somehow untestable. How can we verify if the activity is created successfully - the return type is void? This is an error in the design - only spotted now when we attempt to write a test. Had we written the tests first then we are unlikely to have written the method in this way.
First, lets correct the implementation:
public Activity createActivity(Long id, String type, String location, double distance)
{
Activity activity = null;
Optional<User> user = Optional.fromNullable(userIndex.get(id));
if (user.isPresent())
{
activity = new Activity (type, location, distance);
user.get().activities.put(activity.id, activity);
activitiesIndex.put(activity.id, activity);
}
return activity;
}
Now we can write the test:
@Test
public void testAddActivity()
{
User marge = pacemaker.getUserByEmail("marge@simpson.com");
assertNotNull(marge);
Activity activity = pacemaker.createActivity(marge.id, activities[0].type, activities[0].location, activities[0].distance);
assertNotNull(activity);
Activity returnedActivity = pacemaker.getActivity(activity.id);
assertNotNull(returnedActivity);
assertEquals(activities[0], returnedActivity);
assertNotSame(activities[0], returnedActivity);
}
This test is probably a little overwrought - and we can reduce the number of asserts to make its operation clearer:
@Test
public void testAddActivity()
{
User marge = pacemaker.getUserByEmail("marge@simpson.com");
Activity activity = pacemaker.createActivity(marge.id, activities[0].type, activities[0].location, activities[0].distance);
Activity returnedActivity = pacemaker.getActivity(activity.id);
assertEquals(activities[0], returnedActivity);
assertNotSame(activities[0], returnedActivity);
}
Now we write a test to add a location to an activity:
@Test
public void testAddActivityWithSingleLocation()
{
User marge = pacemaker.getUserByEmail("marge@simpson.com");
Long activityId = pacemaker.createActivity(marge.id, activities[0].type, activities[0].location, activities[0].distance).id;
pacemaker.addLocation(activityId, locations[0].latitude, locations[0].longitude);
Activity activity = pacemaker.getActivity(activityId);
assertEquals (1, activity.route.size());
assertEquals(0.0001, locations[0].latitude, activity.route.get(0).latitude);
assertEquals(0.0001, locations[0].longitude, activity.route.get(0).longitude);
}
If this passes (try it) we can be more ambitious and test multiple locations:
@Test
public void testAddActivityWithMultipleLocation()
{
User marge = pacemaker.getUserByEmail("marge@simpson.com");
Long activityId = pacemaker.createActivity(marge.id, activities[0].type, activities[0].location, activities[0].distance).id;
for (Location location : locations)
{
pacemaker.addLocation(activityId, location.latitude, location.longitude);
}
Activity activity = pacemaker.getActivity(activityId);
assertEquals (locations.length, activity.route.size());
int i = 0;
for (Location location : activity.route)
{
assertEquals(location, locations[i]);
i++;
}
}
In your test/controllers package, create a new test specifically for persistence:
public class PersistenceTest
{
PacemakerAPI pacemaker;
}
Before writing a test, we introduce a utility method which we will use to create a dataset in pacemakerAPI. This is not a test, but will be called from a test.
void populate (PacemakerAPI pacemaker)
{
for (User user : users)
{
pacemaker.createUser(user.firstName, user.lastName, user.email, user.password);
}
User user1 = pacemaker.getUserByEmail(users[0].email);
Activity activity = pacemaker.createActivity(user1.id, activities[0].type, activities[0].location, activities[0].distance);
pacemaker.createActivity(user1.id, activities[1].type, activities[1].location, activities[1].distance);
User user2 = pacemaker.getUserByEmail(users[1].email);
pacemaker.createActivity(user2.id, activities[2].type, activities[2].location, activities[2].distance);
pacemaker.createActivity(user2.id, activities[3].type, activities[3].location, activities[3].distance);
for (Location location : locations)
{
pacemaker.addLocation(activity.id, location.latitude, location.longitude);
}
}
It creates some users + activities, and then adds a route to one of the activities. We can write a test to see if this is functioning generally as expected:
@Test
public void testPopulate()
{
pacemaker = new PacemakerAPI(null);
assertEquals(0, pacemaker.getUsers().size());
populate (pacemaker);
assertEquals(users.length, pacemaker.getUsers().size());
assertEquals(2, pacemaker.getUserByEmail(users[0].email).activities.size());
assertEquals(2, pacemaker.getUserByEmail(users[1].email).activities.size());
Long activityID = pacemaker.getUserByEmail(users[0].email).activities.keySet().iterator().next();
assertEquals(locations.length, pacemaker.getActivity(activityID).route.size());
}
This should pass. However, we are not using the serializer yet. Before writing the serializer test, we need a simple file deleting utility:
void deleteFile(String fileName)
{
File datastore = new File ("testdatastore.xml");
if (datastore.exists())
{
datastore.delete();
}
}
Now we can write a new test specifically to see if an object model is serialized - and restored - successfully.
@Test
public void testXMLSerializer() throws Exception
{
String datastoreFile = "testdatastore.xml";
deleteFile (datastoreFile);
Serializer serializer = new XMLSerializer(new File (datastoreFile));
pacemaker = new PacemakerAPI(serializer);
populate(pacemaker);
pacemaker.store();
PacemakerAPI pacemaker2 = new PacemakerAPI(serializer);
pacemaker2.load();
assertEquals (pacemaker.getUsers().size(), pacemaker2.getUsers().size());
for (User user : pacemaker.getUsers())
{
assertTrue (pacemaker2.getUsers().contains(user));
}
deleteFile ("testdatastore.xml");
}
This is a complex test, involving the creating of two pacemakerAPI objects. One is populated with complete object graph and serialized. The second loads this graph.
As both are held in memory, we can run through them in the final loop to test for equivalence.
If your test fails, first check that the pushing and popping are done in the correct sequence:
@SuppressWarnings("unchecked")
public void load() throws Exception
{
serializer.read();
activitiesIndex = (Map<Long, Activity>) serializer.pop();
emailIndex = (Map<String, User>) serializer.pop();
userIndex = (Map<Long, User>) serializer.pop();
}
public void store() throws Exception
{
serializer.push(userIndex);
serializer.push(emailIndex);
serializer.push(activitiesIndex);
serializer.write();
}
If your test still fails (i.e. with an ArrayIndexOutOfBounds error), try downloading the 1.4.8 version of xstream and adding it to your build path:
In order to verify that the full object graph is being compared, place breakpoints in the equals() method in User, Activity and Location, and run the test in the debugger.
Do you notice anything unusual?
Only the User.equals() method is being triggered, the activities or locations equals are never called. This is because the equals method in User is incomplete. Here is a revision, which includes the extra method call:
@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;
}
}
This test should still run successfully, but perform a more effective 'deep compare' of the full object graph.
Finally, reflect again on this 'populate' method:
void populate (PacemakerAPI pacemaker)
{
for (User user : users)
{
pacemaker.createUser(user.firstName, user.lastName, user.email, user.password);
}
User user1 = pacemaker.getUserByEmail(users[0].email);
Activity activity = pacemaker.createActivity(user1.id, activities[0].type, activities[0].location, activities[0].distance);
pacemaker.createActivity(user1.id, activities[1].type, activities[1].location, activities[1].distance);
User user2 = pacemaker.getUserByEmail(users[1].email);
pacemaker.createActivity(user2.id, activities[2].type, activities[2].location, activities[2].distance);
pacemaker.createActivity(user2.id, activities[3].type, activities[3].location, activities[3].distance);
for (Location location : locations)
{
pacemaker.addLocation(activity.id, location.latitude, location.longitude);
}
}
This is very clumsy and poorly abstracted code. Are there alternative approaches?
The test classes should align with the packages they are testing. If your project does not resemble the layout below, refactor it:
It should be possible to do this just using drag and drop within the package explorer.
Archive of lab so far:
From last weeks lab, we still have these commands outstanding - sorted based on estimated potential complexity of the implementation:
abbrev name params
---------------------------------------------------
du delete-user (user id)
lius list-user (user id)
la list-activities (user id)
l load ()
s store ()
la list-activities (userid, sortBy: type, location, distance, date, duration)
cff change-file-format (file format: xml, json)
The first three commands are relatively straightforward. We already have tests that exercise the features these commands rely on, to we can proceed to implement these commands in main.
Load and store are also tested reasonably comprehensively via the PacemakerAPITest series, so implementing these commands should be doable with some confidence.
The last two commands are more challenging, and ideally should be tackled from a test first perspective. This means formulating a test case that will exercise the feature, writing the test and then implementing, or partially implementing, the feature such that the test passes.
Still outstanding inn the pacemaker is the support of start date and time. You may choose to postpone this until some of the above commands are tackled, or take it on in advance. Date time handling in Java has had some well recognised challenges:
The joda time library was used to overcome some of the well documented issues with date and time:
Java 8 sees a new date and time library:
This can be seen as a replacement for joda time if Java 8 is available. This is the package summary documentation:
The ideal format, as specified in the specification, pretty prints the output like this:
+----+-----------+----------+-------------------+----------+
| ID | FIRSTNAME | LASTNAME | EMAIL | PASSWORD |
+----+-----------+----------+-------------------+----------+
| 1 | homer | simpsom | homer@simpson.com | secret |
| 2 | marge | simpson | marge@simpson.com | secret |
+----+-----------+----------+-------------------+----------+
+----+------+----------+----------+-------------------------------+----------+-----------------------------------+
| ID | TYPE | LOCATION | DISTANCE | STARTTIME | DURATION | ROUTE |
+----+------+----------+----------+-------------------------------+----------+-----------------------------------+
| 1 | walk | fridge | 0.001 | 2013-10-12T09:00:00.000+01:00 | PT3600S | [23.3,32.3, 23.3,32.5, 23.3,32.6] |
| 2 | walk | bar | 1 | 2013-10-13T09:00:00.000+01:00 | PT4200S | [] |
| 3 | run | work | 2.2 | 2013-10-14T09:00:00.000+01:00 | PT600S | [] |
+----+------+----------+----------+-------------------------------+----------+-----------------------------------+
Writing code to do this could be very tedious. These components here implement convenient solutions to this problem:
... and there are others.
If you are relatively new to Java, incorporating these libraries may be challenging. In next weeks lab we will explore how to introduce one of these libraries into the project.