With my generic Get()
method working as it should, it's now time to bake something that can retrieve a single location by its Id. My test for this looks as follows:
[TestMethod]
public void Get_returns_correct_details_for_a_stored_location()
{
Location l1 = new Location(4);
Location l2 = new Location(2);
Location l3 = new Location(7);
l2.SetExitToLocation(0, l1.Id);
l2.SetExitToLocation(2, l3.Id);
using (QuesterContext dbcontext = new QuesterContext())
{
dbcontext.RemoveRange(dbcontext.Locations);
dbcontext.Locations.Add(l1);
dbcontext.Locations.Add(l2);
dbcontext.Locations.Add(l3);
dbcontext.SaveChanges();
}
LocationRepository locationRepository = new LocationRepository();
Location retrievedLocation = locationRepository.Get(l2.Id);
Assert.AreEqual(l2.Id, retrievedLocation.Id);
Assert.AreEqual(l1.Id, retrievedLocation.ExitInDirection(0));
Assert.AreEqual(l3.Id, retrievedLocation.ExitInDirection(2));
}
...and my initial implementation for the Get(int id)
method simply throws an exception, which makes the test go red.
public Location Get(int id)
{
throw new NotImplementedException();
}
After my first red, I implement the Get(int id)
method:
public Location Get(int id)
{
Location result = null;
using(QuesterContext context = new QuesterContext())
{
result = (from e in context.Locations
where e.Id == id
select e).FirstOrDefault();
}
return result;
}
Running the test with the new implementation gives me - another red. The test fails on my attempt to retrieve the correct exit in direction 0. So what's going on here?
Stepping through the test in the debugger reveals the following: locations l1
through l3
are all set up with correct Ids and exits. My retrievedLocation
, however, has the correct Id, but all exits are set to the default of -1.
Looking at the actual database file, I see that the schema only has a column for the Id - but nothing for the exits. This is not actually all that surprising, when you think about it. I, for one, can't think of a good way to store an array in a database - and I guess the programmers behind Entity Framework couldn't either, so I consider myself in good company here. As a result, the array that holds the information about exits in my Location
class simply gets ignored.
I can see two ways around the problem: I can create individual integer fields for each allowable direction. This strikes me as a horrendous solution; it violates several OOD principles. If I ever needed more than, say, four directions, I would have to modify my Locations
class to do so, which violates the open/closed principle. Furthermore, the number of directions really shouldn't be defined in the Location class - it belongs in a class that deals with navigation, according to the single responsibility principle.
With the current solution, using an array, I could have worked around it by injecting the number of possible directions into the constructor. With individual integer fields, e.g. int NorthExit
, int SouthExit
etc. that would be impossible, so I'm not going to go there.
Instead, I'm going to use a new class that deals specifically with this problem:
public class Exit
{
public Exit(int directionId, int destinationLocationId)
{
DirectionId = directionId;
DestinationLocationId = destinationLocationId;
}
public int ExitId { get; set; }
public int DirectionId { get; set; }
public int DestinationLocationId { get; set; }
}
That means some major changes in my Location
class, of course. Instead of an array of integers, I now have a List of Exits. And, in case you're wondering why I have made that a private
property instead of a private
field - that's because Entity Framework needs a property to do its magic.
public class Location
{
private List<exit> _Exits { get; set; }
public int LocationId { get; set; }
public Location(int id) : this()
{
LocationId = id;
}
public Location()
{
_Exits = new List<exit>();
}
public int ExitInDirection(int direction)
{
int result = -1;
Exit exitInDirection = (from e in _Exits
where e.DirectionId == direction
select e).FirstOrDefault();
if (exitInDirection != null)
result = exitInDirection.DestinationLocationId;
return result;
}
public void SetExitToLocation(int direction, int destinationLocationId)
{
_Exits.Add(new Exit(direction, destinationLocationId));
}
}
Running my tests after these changes gives me several reds, of course, where I was testing things like invalid indices. With a list, the following tests that deal with array initialisation or an index that's out of bounds are no longer required:
Location_is_constructed_with_all_exits_set_to_no_exit()
Location_returns_no_exit_if_direction_is_negative()
Location_returns_no_exit_if_direction_is_greater_than_available_directions()
CreateOneWayExitToLocation_throws_exception_if_direction_is_negative()
CreateOneWayExitToLocation_throws_exception_if_direction_is_greater_than_available_directions()
I do need to add one test instead, however, to check that if an exit doesn't exist in a specific direction, my ExitInDirection()
method still returns -1
.
[TestMethod]
public void Location_returns_minus_one_if_no_exit_in_that_direction()
{
Location location = new Location(1);
Assert.AreEqual(-1, location.ExitInDirection(-1));
}
One necessary test, however, remains stubbornly red: The one I created at the beginning of this article. But now it's red for a different reason. If I examine the database after creating my locations, I can see that I now have a table for my exits, and that this table is populated correctly when I save the changes to my context. The problem is that when I retrieve a location from my context, it doesn't automatically retrieve the exits as well (even though it automatically stores them when I store the location).
Getting around this is a little bit tricky in EF7, so I'll leave that until the next post. CodeProject