Scaling Back The Yardstick
I have made some good progress on my book club site over the last year. I launched K & M Tavern with a modified version of the code I was developing here. However, I have gotten a bit stuck recently. I am going to start writing about it again to see if it will help me get unstuck.
I talked in the overview post about having a real-world yardstick use as I made decisions about features. The yardstick I mentioned was a book club. I have found, however, that I need to scale that back a bit.
I have found there is a lot to creating a book club application. I think of several features I need to add every time I sit down to work on it. It is hard to prioritize because they are all relevant. So I’ve decided to break it down even further.
Getting to the Core Functionality
I have come to realize that, at it’s core, the application is a blog. It will include several features that are distinct to a book club but the basic functionality is that of a blog. So my scaled down yardstick will be that of a blog. I will increase the scope once I have the blog part down.
So what is the functionality that I need to have a blog? Luckily there are a ton of examples I can look at to answer that question, starting with this very site.
- Write Posts – Obviously the most important part of a blog is the ability to write posts. This functionality is pretty much done. I am using the TinyMCE control to handle the HTML.
- Comment on Posts – This is the next major functionality. Users should be able to comment on posts. This functionality it also done. However, I need to think about whether or not I want to allow nested comments.
- Edit Posts/Comments – This is the next major functionality I need to implement. The author of a post or comment should be able to edit it. I have worked on it a little bit but haven’t quite gotten it to work yet.
- View Previous Posts – Right now, the home page of K & M Tavern lists every post I have written. This is not a big deal yet but will be soon. It needs to only show the latest 5 or 10 and then allow the user to click a link to page through previous posts.
- View Archive – Just about every blog I have visited has an “Archives” list in its sidebar. This list allows the user to find old posts without having to click through a ton of pages.
- Subscribe – There needs to be an easy way for the user to find out about new posts on the site. Most blogs use RSS feeds for this. I need to decide what kind of RSS feeds will be available (1 per user, 1 per game, etc.).
Those are the major features I need to make sure my application has before I move on to more book club specific functionality. Of course, it has brought up a question in my mind. Why build this when there are perfectly good blogging platforms like WordPress out there? What is comes down to is flexibility. While the core of this application is a blog, it is going to be much more than that. I don’t want to get stuck using something that I can’t completely customize.
So, my goal for this week is to get the editing working. It shouldn’t be too hard. I have had some problems with the TinyMCE control but haven’t tried too hard to fix it. Hopefully it will be an easy fix.
To Do – Adding Features
I have been making some good progress on my Book Club application over the last month even though I haven’t been writing about it. I will get back to writing about my progress soon but have been feeling a pull to get back to my To Do application first.
I have been feeling the constraints of my simple To Do list for a few weeks. It works but there are a few features my process is telling me it needs.
The first one is simple. When I complete a task, it is moved to the bottom of the list and marked through so I will know it’s completed. I knew when I first built the app there would come a point where the list would mostly be completed items. I wasn’t sure whether it would matter, though, so I left it that way. I have reached the point where it’s bugging me so I am going to remove completed items from the list. Luckily, it’s an easy one line fix:
toDos.DataSource = user.ToDoItems.OrderBy(td => td.IsComplete).ThenBy(td => td.ToDoText);
Changed to:
toDos.DataSource = user.ToDoItems.Where(td => !td.IsComplete).OrderBy(td => td.ToDoText);
The next feature is more complex. I have come to the realization that I need some form of grouping for my tasks. I need to be able to put them in projects and possibly contexts to be able to work affectively. The question is what groupings will work best?
In Getting Things Done, David Allen talks about contexts. Assigning a context to a task allows you to group tasks by where you are or what you are doing. For example, you could have a context called @phone. Every call you need to make would go in that context. As you sit down to make calls, all the calls you need to make would be grouped together and easily accessible.
I can see the benefit of this as I look through my current tasks. For example, I have several posts that I want to write on a few different blogs. It would be nice to be able to sort by @writing and see all those posts come up.
I will have to think through contexts more but I want to start with projects. There are a couple questions I need to answer about projects that I will ask in my next post.
Book Club – Adding a Game – Fixing Overlap Bug
In my last post, I added the functionality to check that the game being added did not overlap the currently featured game. However, this was not the logic I intended. The rule I was trying to implement was that the game could not overlap any other game, whether that game was currently featured or not. So I needed to fix that logic.
The first thing I needed to do was change my tests so they were testing the correct logic. Doing this should break them. I changed them to look like this:
[Test]
[ExpectedException(typeof(ConstraintException))]
public void NoOverlapEncompasses() {
var dataContext = new BookClubDataContext();
string game1Name = Guid.NewGuid().ToString();
string game2Name = Guid.NewGuid().ToString();
dataContext.AddGame(game1Name, DateTime.Now.AddDays(4), DateTime.Now.AddDays(10));
dataContext.AddGame(game2Name, DateTime.Now.AddDays(3), DateTime.Now.AddDays(11));
}
[Test]
[ExpectedException(typeof(ConstraintException))]
public void NoOverlapStartDuring() {
var dataContext = new BookClubDataContext();
string game1Name = Guid.NewGuid().ToString();
string game2Name = Guid.NewGuid().ToString();
dataContext.AddGame(game1Name, DateTime.Now.AddDays(4), DateTime.Now.AddDays(10));
dataContext.AddGame(game2Name, DateTime.Now.AddDays(5), DateTime.Now.AddDays(11));
}
[Test]
[ExpectedException(typeof(ConstraintException))]
public void NoOverlapEndDuring() {
var dataContext = new BookClubDataContext();
string game1Name = Guid.NewGuid().ToString();
string game2Name = Guid.NewGuid().ToString();
dataContext.AddGame(game1Name, DateTime.Now.AddDays(4), DateTime.Now.AddDays(10));
dataContext.AddGame(game2Name, DateTime.Now.AddDays(3), DateTime.Now.AddDays(8));
}
These changes caused these tests to fail as expected. So now, how should I fix them?
I originally setup my data context to only return the current game because I didn’t think I need anything more. The code and my tests were telling me otherwise, however. I didn’t have any choice but to listen to them.
I decided I needed two methods. The GetFeaturedGame method I had could remain the same. I know of several places in the app where I will want to get the game that is currently featured. The new method I needed would get the game or games (because it could definitely be more than one) that are set to be featured within a given date range.
I needed a new stored procedure to accomplish thing. Actually, I needed to rename my current procedure. It was returning the currently featured game so I renamed it to GetCurrentlyFeaturedGame. Then, I created a new procedure call GetFeaturedGame. Here is the SQL statement I used:
CREATE PROCEDURE GetFeaturedGame
@StartDate DATETIME,
@EndDate DATETIME
AS
DECLARE @FormattedStartDate DATETIME,
@FormattedEndDate DATETIME
SELECT @FormattedStartDate = CAST(CONVERT(VARCHAR, @StartDate, 110) AS SMALLDATETIME),
@FormattedEndDate = CAST(CONVERT(VARCHAR, @EndDate, 110) AS SMALLDATETIME)
SELECT *
FROM Game
WHERE @FormattedStartDate BETWEEN FeaturedStartDate AND FeaturedEndDate OR
@FormattedEndDate BETWEEN FeaturedStartDate AND FeaturedEndDate OR
FeaturedStartDate BETWEEN @FormattedStartDate AND @FormattedEndDate
I added that to my DBML file. Then I created a public method to call this procedure:
public IEnumerableGetFeaturedGame(DateTime? StartDate, DateTime? EndDate) { return getFeaturedGame(StartDate, EndDate).ToArray(); }
Next, I was able to change my checkOverlap method to use this new method:
bool checkOverlap(DateTime? featuredStartDate, DateTime? featuredEndDate) {
var games = GetFeaturedGame(featuredStartDate, featuredEndDate);
if (games != null && games.Count() > 0) {
return true;
}
return false;
}
Finally, I needed to make a change to my AddGame method to handle the possibility that there is more than one game featured within the date range the user is trying to use. The final method looks like this:
public Game AddGame(string Name, DateTime? FeaturedStartDate, DateTime? FeaturedEndDate, string ImageURL) {
var foundGame = FindGame(Name);
if (foundGame != null) {
throw new DuplicateNameException(string.Format("{0} already exists.", Name));
}
if (checkOverlap(FeaturedStartDate, FeaturedEndDate)) {
var featuredGames = GetFeaturedGame(FeaturedStartDate, FeaturedEndDate);
StringBuilder message = new StringBuilder();
message.Append("A feature game already exists for the given date range.\r\n");
foreach (var featuredGame in featuredGames) {
message.AppendFormat("{0} - {1:MM/dd/yyy} - {2:MM/dd/yyy}\r\n", featuredGame.Name, featuredGame.FeaturedStartDate, featuredGame.FeaturedEndDate);
}
throw new ConstraintException(message.ToString().Substring(0, message.Length - 2));
}
var game = new Game();
game.Name = Name;
game.FeaturedStartDate = FeaturedStartDate != null ? FeaturedStartDate.Value.Date : FeaturedStartDate;
game.FeaturedEndDate = FeaturedEndDate != null ? FeaturedEndDate.Value.Date : FeaturedEndDate;
game.ImageURL = ImageURL;
Games.InsertOnSubmit(game);
SubmitChanges();
return game;
}
All my tests passed. I think I am finally done with this feature. We will see as development continues.
Book Club – Adding a Game – Checking for Overlaps
I have one more rule I need to tackle to finish the AddGame functionality. I didn’t include this in the last post because it complicates things. The rule is that there can only be one featured game at a time.
This threw me off on the creation of my tests. In fact, this is the kind of thing that has caused me to scrap TDD altogether in the past. How do I implement this rule with breaking all my other tests?
For example, my first test was a simple one to make sure a game was added when the DataContext.AddGame method was called. If I implement this rule, that test will not be that simple. It would need to know what all is the in database so that it can enter a valid game to perform the correct test. It would have to know too much about the system to really be effective.
I could fix that test by just passing in null for each of the dates. The test would pass and the overlapping test would not be effective. I actually did this but then changed it back because it broke the test. It is supposed to test that the game was added successfully. Adding a game with no dates does not test that the dates were set correctly.
So what do I do? The more I thought about it, the more I felt I would need to clear the Game table of the database before each test to really get an accurate view of the app. So that’s what I did. The code looks like this:
[SetUp]
public void Initialize() {
using (SqlConnection connection = new SqlConnection("server=localhost;database=BookClub;Integrated Security=true;")) {
connection.Open();
using (SqlCommand command = new SqlCommand("DELETE FROM Game", connection)) {
command.ExecuteNonQuery();
}
connection.Close();
}
}
Quick and dirty. [SetUp] is the command to tell NUnit to run this function before each test. I don’t really like doing this but it’s the quickest way for me to make sure each test is only testing what it’s supposed to without having a lot of extraneous knowledge about the app.
So I decided needed 3 tests to make sure I covered all my bases. I need to check if the start date or end date is within an already featured range and if the range fully encompasses another range. Here are the 3 tests I wrote:
[Test]
[ExpectedException(typeof(ConstraintException))]
public void NoOverlapEncompasses() {
var dataContext = new BookClubDataContext();
string game1Name = Guid.NewGuid().ToString();
string game2Name = Guid.NewGuid().ToString();
dataContext.AddGame(game1Name, DateTime.Now.Date, DateTime.Now.AddDays(3).Date);
dataContext.AddGame(game2Name, DateTime.Now.AddDays(-5).Date, DateTime.Now.AddDays(-5).AddMonths(1).Date);
}
[Test]
[ExpectedException(typeof(ConstraintException))]
public void NoOverlapStartDuring() {
var dataContext = new BookClubDataContext();
string game1Name = Guid.NewGuid().ToString();
string game2Name = Guid.NewGuid().ToString();
dataContext.AddGame(game1Name, DateTime.Now.Date, DateTime.Now.AddMonths(1).Date);
dataContext.AddGame(game2Name, DateTime.Now.AddDays(5).Date, DateTime.Now.AddDays(5).AddMonths(1).Date);
}
[Test]
[ExpectedException(typeof(ConstraintException))]
public void NoOverlapEndDuring() {
var dataContext = new BookClubDataContext();
string game1Name = Guid.NewGuid().ToString();
string game2Name = Guid.NewGuid().ToString();
dataContext.AddGame(game1Name, DateTime.Now.Date, DateTime.Now.AddMonths(1).Date);
dataContext.AddGame(game2Name, DateTime.Now.AddDays(-5).Date, DateTime.Now.AddDays(-5).AddMonths(1).Date);
}
Next, I need to write the code to pass these tests. First, I added the check to the AddGame method:
if (checkOverlap(FeaturedStartDate, FeaturedEndDate)) {
var featuredGame = GetFeaturedGame();
throw new ConstraintException(string.Format("A feature game already exists for the given date range. {0} - Start Date {1:MM/dd/yyy} - End Date {2:MM/dd/yyy}",
featuredGame.Name, featuredGame.FeaturedStartDate, featuredGame.FeaturedEndDate));
}
Then I wrote the checkOverlap method:
bool checkOverlap(DateTime? featuredStartDate, DateTime? featuredEndDate) {
var overlaps = false;
var game = GetFeaturedGame();
if (game != null) {
overlaps = (game.FeaturedStartDate <= featuredStartDate && game.FeaturedEndDate >= featuredStartDate) ||
(game.FeaturedStartDate <= featuredEndDate && game.FeaturedEndDate >= featuredEndDate) ||
(featuredStartDate <= game.FeaturedStartDate && featuredEndDate >= game.FeaturedStartDate);
}
return overlaps;
}
As you can see, I have a new method, GetFeaturedGame. This method returns the currently featured game. Of course, I needed to write the tests for it first. There are 3 tests that are important. It needs to return the featured game when today is the FeaturedStartDate, FeaturedEndDate or between them:
[Test]
public void GetFeaturedGame() {
var dataContext = new BookClubDataContext();
dataContext.AddGame("Test Game", DateTime.Now.AddDays(-5), DateTime.Now.AddDays(5));
var featuredGame = dataContext.GetFeaturedGame();
Assert.IsNotNull(featuredGame, "Feature game not found");
Assert.AreEqual("Test Game", featuredGame.Name);
}
[Test]
public void GetFeaturedGameStartingToday() {
var dataContext = new BookClubDataContext();
dataContext.AddGame("Test Game", DateTime.Now, DateTime.Now.AddDays(5));
var featuredGame = dataContext.GetFeaturedGame();
Assert.IsNotNull(featuredGame, "Feature game not found");
}
[Test]
public void GetFeaturedGameEndingToday() {
var dataContext = new BookClubDataContext();
dataContext.AddGame("Test Game", DateTime.Now.AddDays(-5), DateTime.Now);
var featuredGame = dataContext.GetFeaturedGame();
Assert.IsNotNull(featuredGame, "Feature game not found");
}
I thought about different ways to handle this but settled on using a stored procedure. The other option would be to pull all the games from the database and loop through them. A stored procedure made more sense to me. Here is the SQL statement I used:
SELECT * FROM Game WHERE GETDATE() BETWEEN FeaturedStartDate AND FeaturedEndDate
I added that procedure to the DBML file and created a method to call it:
public Game GetFeaturedGame() {
var games = getFeaturedGame().ToArray();
if (games.Length != 0) {
return games.ElementAt(0);
}
else {
return null;
}
}
So I ran my tests and one of them failed! I actually expected it to but wanted to make sure I was right before I did any coding. It was GetFeaturedGameEndingToday. This failed because of the GETDATE() function in the SQL statement. That statement returns the current date and time. So I need edit to compare the dates only, not the time. This brought up another small change I needed to make.
First, I changed my SQL statement to be this:
SELECT * FROM Game WHERE CAST(CONVERT(VARCHAR, GETDATE(), 110) AS SMALLDATETIME) BETWEEN FeaturedStartDate AND FeaturedEndDate
This will return today’s date with the time of 12:00 am. Then, I needed to make sure the FeaturedStartDate and FeaturedEndDate do not include a time either. I changed both the AddGame test and method to look like this:
[Test]
public void AddGame() {
var dataContext = new BookClubDataContext();
var gameCount = dataContext.Games.Count();
var name = Guid.NewGuid().ToString();
var game = dataContext.AddGame(name, DateTime.Now, DateTime.Now.AddDays(5));
dataContext = new BookClubDataContext();
Assert.AreEqual(gameCount + 1, dataContext.Games.Count(), string.Format("Game was not added. {0} expected - {1} actual", gameCount + 1, dataContext.Games.Count()));
Assert.AreEqual(name, game.Name, "Name was not set correctly");
Assert.AreEqual(DateTime.Now.Date, game.FeaturedStartDate, "FeaturedStartDate not set correctly");
Assert.AreEqual(DateTime.Now.AddDays(5).Date, game.FeaturedEndDate, "FeaturedEndDate not set correctly");
}
public Game AddGame(string Name, DateTime? FeaturedStartDate, DateTime? FeaturedEndDate) {
var foundGame = FindGame(Name);
if (foundGame != null) {
throw new DuplicateNameException(string.Format("{0} already exists.", Name));
}
if (checkOverlap(FeaturedStartDate, FeaturedEndDate)) {
var featuredGame = GetFeaturedGame();
throw new ConstraintException(string.Format("A feature game already exists for the given date range. {0} - Start Date {1:MM/dd/yyy} - End Date {2:MM/dd/yyy}",
featuredGame.Name, featuredGame.FeaturedStartDate, featuredGame.FeaturedEndDate));
}
var game = new Game();
game.Name = Name;
game.FeaturedStartDate = FeaturedStartDate != null ? FeaturedStartDate.Value.Date : FeaturedStartDate;
game.FeaturedEndDate = FeaturedEndDate != null ? FeaturedEndDate.Value.Date : FeaturedEndDate;
Games.InsertOnSubmit(game);
SubmitChanges();
return game;
}
So there it is. All my tests passed. I’m done. Or am I?
I created an AddGame.aspx page in my project and started adding games. As I was adding them, a bug in my logic. It’s not that hard to pick up on once I found it.
The overlap tests only works on the currently featured game. However, it needs to work on all games. I will detail how I fixed this in my next post.
Book Club – Adding a Game
Now that I have my project setup, it’s time to start adding functionality. Of course I almost fall off the rails before I even get started.
As I started thinking about this functionality, my mind went crazy. Something like this:
The user needs to be able to add a game but I am going to want to add functionality to handle more than one book club because I want to allow people to sign-up and create clubs so I need to allow them to create those clubs and then have the game tied to club. Or maybe everyone can see every game in database so whenever anyone adds a game everyone benefits but what if the club is only geared towards a specific genre of game and they don’t want to see everything. I’ll need to…
And on and on. This kills so many projects for me. So I had to pull myself back and ask the question, what is the bare minimum I need to be able to add games to the application? What information is really important?
There is a lot of information I could add. There is the name of the game, genre, developer and publisher companies with links to their official sites, release date, a description, a link to the official site or important fan pages, ESRB rating, system requirements, and more. But what do I really need to accomplish my goal while keeping with the book club theme?
After going back and forth about what to add, I decided there is only really one thing the application absolutely needs – the name of the game. Everything else would be nice for informational purposes but is not absolutely necessary for the app to function.
So I know that my game table needs to include the name of the game. The other thing I need to be able to do is mark a game as currently featured. This is the game we are currently playing through. It will be feature on the home page so everyone knows what it is.
I thought of two ways to handle this. The easiest would be to have a bit field that is set to true on the featured game. Simple enough to do. But is it what I need?
The other option would be to include a “Featured Start Date” and “Featured End Date” with each game. This would allow the user to setup several games to be featured back-to-back without having to worry about marking each one when the time comes. This would be more complicated than just adding a bit field. Would it be better though?
I think it is. The main reason is the voting system. I want the users to be able to vote on what game is going to be featured next. I want to be able to close the voting down before the current feature is over to give people a chance to get the game and play some before it is featured. The dates allow me queue the next game up without removing the current one.
The other area where it’s better is in the club history. I don’t want to allow games to be featured more than once. I will need a way to know that a game has been featured in the past. Otherwise a large enough group of users that love a specific game could keep it as the featured game for several cycles.
So my extremely simple Game table looks like this:
GameID is an identity field so it increments every time a game is entered. FeaturedStartDate and FeaturedEndDate allow nulls so the user can add a game without scheduling it to be featured. This would allow the user to add a bunch of games for people to vote on.
I was wondering how big I should make the Name field so I looked up “longest game names”. The first result was an article on Games Radar called “The Longest Game Names Of All Time”. The longest name they list is “Advanced Dungeons & Dragons: Eye of the Beholder II -- The Legend of Darkmoon”, which, if I’ve counted correctly, has 77 characters. So I took that and rounded up to 80. Hopefully that will cover everything.
OK, time to start coding. I want to do this project right so I am going to try TDD as much as possible. I’ve not had a lot of luck with this method in the past but would like the peace of mind that comes from a large group of passing tests.
The first thing I needed to do was add the project to hold all my tests. I created a class library project called BookClub.Test. I am using NUnit to run my tests so I added a reference to nunit.framework.
Now, the idea is that you write a test and watch it fail before you do any coding. I haven’t found that to be completely practical. The problem is the tests won’t compile unless there is some code there in the first place. So before I wrote my first test I added my business layer DLL, BookClub.Business.
I am using LINQ to SQL so I created a DBML file, BookClub.dbml. I then added my Game table to it, which looks like this:
The functionality I was adding was to add a game to the database. In LINQ, this is done by creating an instance to a data context, creating an instance of the object to be inserted, populating that objects members, adding that object to the data context through Games.InsertOnSubmit(), and then calling the data context’s SubmitChanges method.
In the past I have done most of this work in the events of the UI. I have found this to lead to a lot of business information being exposed to the UI. I wanted to take as much of that away from the UI as possible so I decided to add an AddGame method to the data context. That way the UI doesn’t have to worry about what goes into adding the game. It just creates an instance of the data context and calls the method with the appropriate parameters.
Here is the basic code to create the partial class with the method I want:
using System;
using System.Data;
using System.Linq;
namespace BookClub.Business {
///
/// Book Club Data Context
///
partial class BookClubDataContext {
///
/// Adds a new game to the database
///
///
Name of the game
///
Date the game will become the featured game. null is valid if this has not been determined yet
///
Date the game will stop being the featured game. null is valid if this has not been determined yet
/// A Game object if it was added successfully, null otherwise
public Game AddGame(string Name, DateTime? FeaturedStartDate, DateTime? FeaturedEndDate) {
return null;
}
}
}
So I had the basic structure that will allow me to write my tests. The first test was pretty simple:
////// Test adding a game. /// [Test] public void AddGame() { var dataContext = new BookClubDataContext(); var gameCount = dataContext.Games.Count(); var name = Guid.NewGuid().ToString(); var game = dataContext.AddGame(name, DateTime.Now.Date, DateTime.Now.AddDays(2).Date); dataContext = new BookClubDataContext(); Assert.AreEqual(gameCount + 1, dataContext.Games.Count(), string.Format("Game was not added. {0} expected - {1} actual", gameCount + 1, dataContext.Games.Count())); Assert.AreEqual(name, game.Name, "Name was not set correctly"); Assert.AreEqual(DateTime.Now.Date, game.FeaturedStartDate, "FeaturedStartDate was not set correctly"); Assert.AreEqual(DateTime.Now.AddDays(2).Date, game.FeaturedEndDate, "FeaturedEndDate was not set correctly"); }
Then I finished out the AddGame method:
public Game AddGame(string Name, DateTime? FeaturedStartDate, DateTime? FeaturedEndDate) {
var game = new Game();
game.Name = Name;
game.FeaturedStartDate = FeaturedStartDate;
game.FeaturedEndDate = FeaturedEndDate;
Games.InsertOnSubmit(game);
SubmitChanges();
return game;
}
Not much to it here but I test actually helped me find a bug in the initial version. I was trying to go as fast as I could so I was copying lines and changing variables to speed things up. In the course of doing that I actually set both the start date and end date to the same date. This obviously caused my test to fail. My first test kept me from having a small bug haunt me down the line. Nice.
Alright, basic functionality for adding a game was complete. Now I needed to add some business rules. The most obvious one is that the start date should be before the end date. The check for that is done in the OnValidate method of the Game class. First, the test:
////// Test that the featured start date is before the featured end date. The app should throw an error /// [Test] [ExpectedException(typeof(ConstraintException))] public void FeaturedStartBeforeEnd() { var dataContext = new BookClubDataContext(); dataContext.AddGame("Blah", DateTime.Now.Date, DateTime.Now.AddDays(-1).Date); }
Then here is the code to pass the test:
using System;
using System.Data;
using System.Data.Linq;
namespace BookClub.Business {
///
/// Game Partial Class
///
partial class Game {
partial void OnValidate(ChangeAction action) {
if (FeaturedEndDate < FeaturedStartDate) {
throw new ConstraintException("FeaturedEndDate cannot be before FeatureStartDate");
}
}
}
}
The next rule is to not allow duplicate games. Here’s the test:
////// Duplicate games should not be added /// [Test] [ExpectedException(typeof(DuplicateNameException))] public void NoDuplicates() { var dataContext = new BookClubDataContext(); var game = dataContext.Games.First(); dataContext.AddGame(game.Name, game.FeaturedStartDate, game.FeaturedEndDate); }
Passing this test took a little more work than the previous ones. First off, I needed a way to search for a game based on the name. This led to a new function in my data context, FindGame. But, of course, I need a test before I can write that function:
////// Test finding a game by the name /// [Test] public void FindGame() { var dataContext = new BookClubDataContext(); var game = dataContext.Games.First(); var foundGame = dataContext.FindGame(game.Name); Assert.IsNotNull(foundGame); Assert.AreEqual(game.Name, foundGame.Name); Assert.AreEqual(game.FeaturedStartDate, foundGame.FeaturedStartDate); Assert.AreEqual(game.FeaturedEndDate, foundGame.FeaturedEndDate); foundGame = dataContext.FindGame("Blah blah blah"); Assert.IsNull(foundGame); }
Now I can write my function:
////// Find the game with the given name /// /// Name of the game ///Game if found, null otherwise public Game FindGame(string Name) { Game game = null; try { game = Games.First(g => g.Name.ToLower() == Name.ToLower()); } catch { } return game; }
I’m not convinced this is the best way to do this but it works for now.
My FindGame tests passed so I could finish the AddGame functionality to pass the duplicate check test. I just added a little code to the AddGame method, which ended up like this:
////// Adds a new game to the database /// /// Name of the game /// Date the game will become the featured game. null is valid if this has not been determined yet /// Date the game will stop being the featured game. null is valid if this has not been determined yet ///A Game object if it was added successfully, null otherwise public Game AddGame(string Name, DateTime? FeaturedStartDate, DateTime? FeaturedEndDate) { var foundGame = FindGame(Name); if (foundGame != null) { throw new DuplicateNameException(string.Format("{0} already exists.", Name)); } var game = new Game(); game.Name = Name; game.FeaturedStartDate = FeaturedStartDate; game.FeaturedEndDate = FeaturedEndDate; Games.InsertOnSubmit(game); SubmitChanges(); return game; }
I was feeling pretty good about this code until I remembered one more rule I needed to add. There needs to be only one featured game at a time. This rule throws a bit of a monkey wrench into things.
In fact, it’s enough of a wrench that I decided to put it off for a little bit. In my next post, I will describe the issues this presents and how I dealt with them.