Saturday, January 02, 2016

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:

...and my initial implementation for the Get(int id) method simply throws an exception, which makes the test go red.

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

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:

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.

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.

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.