Objectives

Expand the pacemaker model to include Location and Activity classes. Introduce a set of tests to verify the behavior of the model.

Activity & Location classes

Introduce these classes into your model:

Location

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);  
  } 
}

Activity

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);  
  } 
}

User & PacemakerAPI

In Users, introduce the following new field:

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

PacemakerAPI

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 .

LocationTest

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:

Fixtures

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.

UserTest & ActivityTest

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.

Static Fixtures

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:

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.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());
  }
}

ActivityTest

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());
  }
}

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<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.

First PacemakerAPI Test

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);

More PacemakerAPITests

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.

Activities Tests

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++;
    }
  }

Exercises

We now have a more substantial body of tests, which should all run:

Archive of lab so far: