Story Details for articles

Golf Tracker - Ep. 4 - Unit Testing

kahanu
Author:
Version:
Views:
2926
Date Posted:
8/24/2011 7:49:33 PM
Date Updated:
8/24/2011 7:49:33 PM
Rating:
0/0 votes
Framework:
ASP.NET MVC 2
Platform:
Windows
Programming Language:
C#
Technologies:
NUnit
Tags:
unit tests, nunit, ASP.NET MVC, C#, golf, golf tracker
Demo site:
Home Page:
Share:

Golf Tracker - Repository Unit Testing

If you haven't heard, unit testing is very important to successful application development. 

Unit testing I consider to be an art.  Some people are experts at it, some are ignorant to it, and some like me, are just better than average at it.

Without going into too much detail about unit testing since this video doesn't cover what it is, I will say that the basic premise of unit testing is to create a test against your classes that have no dependency on anything physical such as a working database.  Everything that you test against should be either mocked or faked.  I'll talk about that in a second.

The primary thing you want to do in unit testing, is to create the smallest test you can on your classes, just to the point that it passes, and then move on to another test.

There are two primary thoughts for unit testing that you should follow:
  • RED - make sure the first test fails.  You want to make sure that your tests can fail.  If you test passes without you doing much of anything, then something is wrong with your test.
  • GREEN - make it pass.  Do what you can to make the test pass.
  • REFACTOR - continue with more tests.  Once you make a test pass, move on and create another test.  If a previously Green test fails, this is most likely due to later refactoring to your application.  This is expected and it needs to be addressed.  There will be many times when you runs your tests that you will see many more failing tests than there should be.  When this happens, simply go back into those tests and do what is needed to make them pass.
The next thought is how you structure each of your tests.  Like mocking frameworks, and testing frameworks, there are different ways to handle this, but the way I'm going to demonstrate is the triple-A method.
  • Arrange - arrange the code necessary to run each test.  This includes instantiating of classes, setting local variables, etc.
  • Act - act on a method.  Once everything is arranged properly, you can execute a method call or something like that to get a result.
  • Assert - assert the result is as expected.  Make sure that the result from the method call, is what is expected.

Initiating Tests

What I'll demonstrate here is how to create repository unit tests using fake data.

There are several ways to create unit tests, against mocks or against fakes.  Which one you use it entirely up to you but it will also depend on your skill set.  Mocking takes a bit more insight in a particular mocking framework (in which there are many) and a good knowledge of the framework you are working with that you want to mock.

Fakes on the other hand can be created fairly simply.  Fakes are generally classes that implement existing interfaces that actually contain fake data to emulate a working data source.  Many times I prefer working with fakes so I can test whether actual code is working and returning data the way I need.

In this set of tests I'll be faking the ICourseRepository interface and running tests against a few different methods.

I like to build a structure to my fakes that make it easy to refactor when necessary.  I generally build separate classes that contain some fake data, the concrete class that implements the interface and the unit test class.

course unit tests folder
In the image above, the CourseTests.cs file is the unit tests file, the FakeCourseData.cs file will contain fake data that emulates a data store such as SQL Server, and the FakeCourseRepository.cs file will implement the ICourseRepository interface.

Let me show you the FakeCourseData.cs code so you can see what I'm doing here.

01.using System;
02.using System.Collections.Generic;
03.using System.Linq;
04.using GolfTracker.DataObjects.LinqToSql;
05. 
06.namespace GolfTracker.UnitTests.Course_Tests
07.{
08.    public class FakeCourseData
09.    {
10.        public IQueryable<GolfTracker.DataObjects.LinqToSql.Course> GetCourses()
11.        {
12.            IList<GolfTracker.DataObjects.LinqToSql.Course> courseList = new List<GolfTracker.DataObjects.LinqToSql.Course>();
13.            GolfTracker.DataObjects.LinqToSql.Course course = null;
14. 
15.            for (int i = 0; i < 10; i++)
16.            {
17.                course = new GolfTracker.DataObjects.LinqToSql.Course();
18.                course.ID = GetGuid()[i];
19.                course.CourseName = "Course " + i.ToString();
20.                course.VoteCount = 20;
21.                course.VoteTotal = 74;
22.                course.Location = "Location " + i.ToString();
23.                courseList.Add(course);
24.            }
25. 
26.            return courseList.AsQueryable();
27.        }
28. 
29. 
30.        private Guid[] GetGuid()
31.        {
32.            Guid[] guidList = new Guid[]
33.            {
34.                new Guid("2b7b2486-57be-4470-8096-181548774534"),
35.                new Guid("52832e96-f031-4e42-97fc-5d810aaa70f1"),
36.                new Guid("6b518cf1-9323-4b25-b7c7-f7678910f01f"),
37.                new Guid("96d63a27-7028-46a1-b72e-3e10eafac224"),
38.                new Guid("3591b0e3-a220-409b-9049-efd1eaf53e50"),
39.                new Guid("ed95e452-4675-4bc1-ba1b-9fce741ae51a"),
40.                new Guid("8f5c4e03-3e65-4a31-91d0-b8a1587d24b1"),
41.                new Guid("5979c0a7-afbf-4da2-a180-dae95c613d8f"),
42.                new Guid("7a16111d-8a63-4a55-a9e9-8027e99ded74"),
43.                new Guid("e501f00b-168f-4699-9980-df2b4015cf65")
44.            };
45.            return guidList;
46.        }
47. 
48.    }
49.}

The GetCourses() method returns an IQueryable<Course> and it creates 10 courses.  This will of course be embellished over time as needs arise but for now, it will suit our needs. 

Since I've chosen to have the primary keys as Guids, I need to hard code the Guids into the data instead of simply calling Guid.NewGuid().  By hard coding them, it allows me to call the GetByID(Guid id) method to return a single Course.  If the ID of the course was being generated by Guid.NewGuid(), it would be generating a random Guid every time and I would not be able to query against it.

The FakeCourseRepository looks like this.

01.using System.Linq;
02.using GolfTracker.DataObjects.Interfaces;
03.using GolfTracker.DataObjects.Mappers;
04. 
05.namespace GolfTracker.UnitTests.Course_Tests
06.{
07.    public class FakeCourseRepository : ICourseRepository
08.    {
09.        IQueryable<GolfTracker.DataObjects.LinqToSql.Course> courseData = null;
10. 
11.        public FakeCourseRepository(IQueryable<GolfTracker.DataObjects.LinqToSql.Course> courses)
12.        {
13.            this.courseData = courses;
14.        }
15. 
16.        #region ICRUDRepository<Course> Members
17. 
18.        public IQueryable<GolfTracker.BusinessObjects.Course> GetAll()
19.        {
20.            return courseData.Select(c => CourseMapper.ToBusinessObject(c));
21.        }
22. 
23.        public GolfTracker.BusinessObjects.Course GetById(Guid id)
24.        {
25.            return CourseMapper.ToBusinessObject(courseData.SingleOrDefault(c => c.ID == id));
26.        }
27. 
28.        public Guid Insert(GolfTracker.BusinessObjects.Course model)
29.        {
30.            return Guid.NewGuid();
31.        }
32. 
33.        public Guid Update(GolfTracker.BusinessObjects.Course model)
34.        {
35.            return model.ID;
36.        }
37. 
38.        public void Delete(GolfTracker.BusinessObjects.Course model)
39.        {
40.            throw new NotImplementedException();
41.        }
42. 
43.        #endregion
44.    }
45.}

Being a good programmer, I want to follow production-like procedures as much as possible and in this case I implement a pseudo-dependency injection into the class for the data.  This way I can swap out any data source that I want to test against.

In this example, at this moment, only the to GET methods are actually calling methods from the data source.  The Insert and Update methods are simply returning passed in data or creating data on the fly.  These can be embellished in the future as needed, but for now this is fine.

Here are some simple tests to get started.

01.using System;
02.using System.Linq;
03.using GolfTracker.DataObjects.Interfaces;
04.using NUnit.Framework;
05.using NUnit.Framework.SyntaxHelpers;
06. 
07.namespace GolfTracker.UnitTests.Course_Tests
08.{
09.    [TestFixture]
10.    public class CourseTests
11.    {
12.        ICourseRepository GetRepository()
13.        {
14.            return new FakeCourseRepository(new FakeCourseData().GetCourses());
15.        }
16. 
17.        [Test]
18.        public void Course_List_Contains_10_Courses()
19.        {
20.            var repository = GetRepository();
21. 
22.            var courses = repository.GetAll();
23. 
24.            Assert.AreEqual(10, courses.Count());
25.        }
26. 
27.        [Test]
28.        public void Returned_Object_Type_Is_Of_Type_BusinessObject()
29.        {
30.            var repository = GetRepository();
31. 
32.            var course = repository.GetById(new Guid("96d63a27-7028-46a1-b72e-3e10eafac224"));
33. 
34.            Assert.IsInstanceOfType(typeof(GolfTracker.BusinessObjects.Course), course);
35.        }
36. 
37.        [Test]
38.        public void Course_Name_Equals_TPC()
39.        {
40.            var repository = GetRepository();
41. 
42.            var course = repository.GetById(new Guid("3591b0e3-a220-409b-9049-efd1eaf53e50"));
43. 
44.            Assert.AreEqual("Course 4", course.CourseName);
45.        }
46.    }
47.}

These tests start out with some simple assertions.
  • Assert the total records - get the number of records back and assert that the count is what is expected.
  • Assert the returned type - get the Type that is being returned and make sure it is of the Course Type.
  • Assert the course name - get a single Course and assert the CourseName value is what is expected.
There are several ways to run these tests.  You can either install the Visual Studio add-in from TestDriven.net which allows you to right-click inside the test methods and run each test, or install and the NUnit GUI.

If you install the NUnit GUI, it allows you to quickly see how all your tests are doing and which tests are failing if any.

When you open NUnit GUI, select File --> Open and locate your unit tests DLL file.  In this case, I'll select the GolfTracker.UnitTests.dll and click OK.  This is what you will see once the file loads.

nunit interface

You'll see that it loads all the methods of the assembly that you opened.  To run the tests, click the large "Run" button.  This is the result.

nunit run

This of course assumes that all the test do pass.  If a tests fails, it will look like this.

nunit test failed

It will tell you which test(s) failed and what the error message is on that test.  Apart from much more sophisticated testing frameworks, this is a simple way of creating unit tests for your application and then running all the tests and keeping an eye on which ones might fail.

This is especially helpful when you start refactoring your application.  By constantly checking your tests this way, you can see if any older tests break.  Refactoring always has the side affect of breaking some older code, and this is a great way of handling those situations.
Now that the unit tests are setup, we can add to them with tests that are needed for continuing development.

Stay tuned for more.

Comments

    No comments yet.

 

User Name:
(Required)
Email:
(Required)