Welcome back! I’ve gone and switched the title of these articles all together to reflect the content more adequately! (Maybe I should have written the series ahead of time to avoid such issues, ya think?)
Anyway, last time we introduced some very basic unit tests. We simply verified that we can indeed create each object and pass in the invariants as constructor parameters. The only real decision making that this influenced was how to create objects. For example, it is no problem at all to allow direct creation (public constructor) on root objects such as Movie, Video, Account, and Customer, however I chose not to let Transaction be created directly, rather it is created from an existing Account object. The reason for this is simple. Picture what happens at a video store: You go to the counter with your card and your videos, the employee scans the card and sees your account details. The next thing that the employee will do is start a new transaction. An account object is always present at the time that we need to get a transaction object. Another reason for this is I decided to satisfy the invariants of the transaction (specifically the account) at creation time.
Jimmy Nilsson talks about creating fluent interfaces in his book Applying Domain Driven Design. We want our object model to express intent and reinforce correct usage. To demonstrate this, consider the following code two segments:
I think that the 2nd option conveys more meaning and is more fluent than the first. This is, however, an early design choice and is always up to change with further refactorings.
I left off last time with a list of passing tests. (Sometimes people suggest to leave a failing test so you know right where to pick up the next day.) I have implemented a few more trivial tests, which I won’t bore you with here. Let’s get on with the more interesting tests. Another thing I’d like to note is that, the tests here pass, but only after I write the appropriate code in the model to satisfy the test. I am purposefully omitting the domain model code at this time because it is unimportant. You should be able to understand what the code does without looking inside the class. Onward….
As mentioned in the 1st article, a transaction will have many rentals. That is because you can rent more than 1 video per visit. The price of renting movies will vary, so in order to have history of what price was paid for an old transaction, we must allow the Rental to carry a price. Another thing we will have to consider is how to determine when an item is due. We’ll have to communicate with some service that will tell us when an item is due, given whether it is a new release, or a hardware item, etc. All we are concerned about at this time is that the rental knows it has a due date. How the due date is calculated is a responsibility outside of the Rental class. Agreed? On with the tests.
This test is a bit longer than previous ones, so I’ll go over it. We start by creating a customer and account to use in our test. Then we use the account object to create a transaction. The next sets the Customer that is actually renting the item. We need this because we may prevent the customer from renting items above his/her rental restriction. I’m now checking to see if the date was set. I used a simple 2 hour date range, because I cannot guarantee how fast this code will run. The idea is that the date and time will change from MinValue to Now after the creation of the Transaction. This will be caught by the assertion. The rest of the Asserts should be self-explanatory.
Now we are able to create a transaction, we should now be able to create rentals and add them to the transaction. Adding the rental to the transaction should change the transaction’s subtotal properly, so we need to test that as well, however this should be broken up into two tests (Remember, try to only test one unit at a time!).
As you can see, we create a rental, add it to the transaction, and verify that the collection count is correct and contains the new rental. We do it twice just to be sure.
One concept I haven’t shown yet is to test invalid usage of the model. We need to do this to make sure our domain model behaves accordingly if it is abused. Here’s a good example:
There is no conceptual reason that a rental should be added twice, so we check to make sure this isn’t allowed.
Up next, we need to verify that the Transaction’s subtotal property is calculated correctly.
This test failed initially, and to get it to pass I had 2 choices of implementation: have a dynamic property that calculates the sum of the rental prices on the fly, or capture the collection adding/removing actions and adjust it appropriately. I chose the latter option, because I would like to persist this as a de-normalized column in the database. This way I can deal with a transaction object without actually loading up all the rentals along with it. You’ll see the benefit of this a bit later when we talk about NHibernate’s lazy loading.
Here’s the (slightly modified) feature list. I’ve colored the already implemented features in green.
- Add new Customer / Account
- Add other members to an account
- Restrict certain members from renting certain content
- Query for a Customer by Customer # (swipe card, etc), phone number, or last name
- Add new rental item (movie, video game, game console, vcr, etc)
- Rent an item to a customer
- Create a Transaction
- Add Rental To Transaction
- Finalize Transaction
- Calculate due date for an item
- Check items back in from a customer
- Get movies checked out for a customer
- Calculate late fees for a customer
- Query for an item, see who has it
We’re coming to a point where we need to start persisting our domain model. Most of the remaining features to implement will employ querying a database, so we’ll probably start on persistence next time.