Expand the pacemaker model to include Location and Activity classes. Introduce a set of tests to verify the behavior of the model.
Introduce these classes into your model:
package models;
import static com.google.common.base.MoreObjects.toStringHelper;
import com.google.common.base.Objects;
public class Location
{
static Long counter = 0l;
public Long id;
public float latitude;
public float longitude;
public Location()
{
}
public Location (float latitude, float longitude)
{
this.id = counter++;
this.latitude = latitude;
this.longitude = longitude;
}
@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);
}
}
ppackage models;
import static com.google.common.base.MoreObjects.toStringHelper;
import java.util.ArrayList;
import java.util.List;
import com.google.common.base.Objects;
public class Activity
{
static Long counter = 0l;
public Long 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)
{
this.id = counter++;
this.type = type;
this.location = location;
this.distance = distance;
}
@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);
}
}
In Users, introduce the following new field:
public Map<Long, Activity> activities = new HashMap<>();
We can now rework the PacemakerAPI to more effectively manage the model. Here is a new version:
package controllers;
import java.util.Collection;
import com.google.common.base.Optional;
import java.util.HashMap;
import java.util.Map;
import models.Activity;
import models.Location;
import models.User;
public class PacemakerAPI
{
private Map<Long, User> userIndex = new HashMap<>();
private Map<String, User> emailIndex = new HashMap<>();
private Map<Long, Activity> activitiesIndex = new HashMap<>();
public PacemakerAPI()
{
}
public Collection<User> getUsers ()
{
return userIndex.values();
}
public void deleteUsers()
{
userIndex.clear();
emailIndex.clear();
}
public User createUser(String firstName, String lastName, String email, String password)
{
User user = new User (firstName, lastName, email, password);
userIndex.put(user.id, user);
emailIndex.put(email, user);
return user;
}
public User getUserByEmail(String email)
{
return emailIndex.get(email);
}
public User getUser(Long id)
{
return userIndex.get(id);
}
public void deleteUser(Long id)
{
User user = userIndex.remove(id);
emailIndex.remove(user.email);
}
public void createActivity(Long id, String type, String location, double distance)
{
Activity activity = new Activity (type, location, distance);
Optional<User> user = Optional.fromNullable(userIndex.get(id));
if (user.isPresent())
{
user.get().activities.put(activity.id, activity);
activitiesIndex.put(activity.id, activity);
}
}
public Activity getActivity (Long id)
{
return activitiesIndex.get(id);
}
public void addLocation (Long id, float latitude, float longitude)
{
Optional<Activity> activity = Optional.fromNullable(activitiesIndex.get(id));
if (activity.isPresent())
{
activity.get().route.add(new Location(latitude, longitude));
}
}
}
Read this class carefully - particularly the createActivity
and addLocation
methods.
Commit and push your project to github with a suitable commit message .
Create a new unit test in the new models package called LocationTest
:
If you use the Eclipse wizards to create the test, 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 ( 23.3f, one.latitude,0.01);
assertEquals ( 33.3f, one.longitude,0.01);
}
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{ 23.3, 33.3,2}", one.toString());
}
All should now be passing:
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 ( 23.3f, one.latitude,0.01,);
assertEquals ( 33.3f, one.longitude,0.01,);
}
@Test
public void testIds()
{
assertNotEquals(one.id, two.id);
}
@Test
public void testToString()
{
assertEquals ("Location{ 23.3, 33.3,2}", one.toString());
}
}
Ready this carefully - the tests are identical, with the setup/tearDown methods called before/after each individual test.
Here is a new version of the UserTest:
package models;
import static org.junit.Assert.*;
import java.util.HashSet;
import java.util.Set;
import org.junit.After;
import org.junit.Before;
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.After;
import org.junit.Before;
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.001, test.distance,0.0001);
}
@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:
Commit and push to github.
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.3f, locations[0].latitude);
assertEquals (0.01, 33.3f, 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 org.junit.Test;
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.001, test.distance, 0.0001);
}
@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 models;
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();
}
@After
public void tearDown()
{
pacemaker = null;
}
@Test
public void testUser()
{
assertEquals (0, pacemaker.getUsers().size());
}
}
This should of course pass and verifies that we are starting with an empty model. We can augment the test:
@Test
public void testUserEmpty()
{
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());
}
Which should also pass...
Our API returns a User object - which we can 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 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 verifies that homer and homer2 are equals (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. Here is a replacement for all of the above:
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.
Commit this to git.
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? 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++;
}
}
We now have a more substantial body of tests, which should all run:
Archive of lab so far: