Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

An Exploration of TDD Part 3 - Discovering that EF7 Doesn't Like Arrays

5.00/5 (1 vote)
2 Jan 2016CPOL3 min read 3.9K  
An Exploration of TDD Part 3 - Discovering that EF7 Doesn't Like Arrays

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:

C#
[TestMethod]
public void Get_returns_correct_details_for_a_stored_location()
{
    // Create some locations
    Location l1 = new Location(4);
    Location l2 = new Location(2);
    Location l3 = new Location(7);

    // Set some random exits between locations
    l2.SetExitToLocation(0, l1.Id);
    l2.SetExitToLocation(2, l3.Id);

    // Set up the database context
    using (QuesterContext dbcontext = new QuesterContext())
    {
        // Clear the Locations table
        dbcontext.RemoveRange(dbcontext.Locations);

        // Add the locations I prepared earlier
        dbcontext.Locations.Add(l1);
        dbcontext.Locations.Add(l2);
        dbcontext.Locations.Add(l3);

        // Save my changes to the locations table
        dbcontext.SaveChanges();
    }

    // Now test that Get() retrieves entries from the database correctly
    LocationRepository locationRepository = new LocationRepository();

    Location retrievedLocation = locationRepository.Get(l2.Id);

    // Check that we have the correct number of locations
    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.

C#
public Location Get(int id)
{
    throw new NotImplementedException();
}

After my first red, I implement the Get(int id) method:

C#
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:

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

C#
public class Location
{
    private List<exit> _Exits { get; set; }

    public int LocationId { get; set; }

    public Location(int id) : this()
    {
        // Store the id of the location
        LocationId = id;
    }

    public Location()
    {
        // Initialise the exits
        _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 an exit in the specified direction exists, return that
        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.

C#
[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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)