Skip to content

Latest commit

 

History

History
181 lines (139 loc) · 6.98 KB

13. Unit testing.md

File metadata and controls

181 lines (139 loc) · 6.98 KB

Unit Testing

One of the main reason why we've designed our code the way we've done, is to be able to unit test all our business logic. The business logic is certainly not the only code that needs to be tested, but it can be argued that it is the most important code to write unit tests for.

Other parts of the code could be tested using integration tests, and having both is probably a good idea.

Unit test setup

In our unit test project, we should have at least one test class for each unit we're testing. The units we are about to test are mostly our Service classes, and we can expect to have (at least) one test class for each Service class.

A unit test class has the following structure:

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace CoursesAPI.Tests.Services
{
	[TestClass]
	public class CourseServicesTests
	{
		[TestInitialize]
		public void Setup()
		{
			// TODO: code which will be executed before each test!
		}

		[TestMethod]
		public void CoursesTestGetListOfCourses()
		{
			// Arrange:

			// Act:

			// Assert:
		}
	}
}

There are a number of points to consider here:

  • The test class can have any name whatsoever. It does need the [TestClass] attribute though.
  • It may contain as many test methods as we whish. They can be called anything we want. They should also return void, and shouldn't accept any parameters. Most importantly, they should be decorated with the [TestMethod] attribute.
  • A test class may also declare a method with the [TestInitialize] attribute. If this method is present, it will get called before each test in the class is executed.
  • Each test method should have three sections: Arrange, Act and Assert (see below).

System Under Test

Each unit test should test a single unit. This unit is sometimes referred to as "System Under Test" or SUT in other literature.

Arrange/Act/Assert

Each test method should have three sections:

  • Arrange - this section makes the necessary arrangements for the test.
  • Act - this section is usually just a single line, which executes the method being tested.
  • Assert - this section asserts that all the postconditions are valid after the given method being tested has executed.

The Arrange section usually contains one or more of the following:

  • Create an instance of the system under test.
  • Create the necessary test data.
  • Declare various parameters/constants which will be used in the Act section.

Test data

Most unit tests require some test data. Test data is usually just a collection of objects, which are then fed to a MockUnitOfWork:

		[TestMethod]
		public void CoursesTestGetListOfCourses()
		{
			// Arrange:
			var waitingList = new List<CourseInstanceWaitingList>
			{
			  new CourseInstanceWaitingList
			  {
			    // TODO: test data instance
			  }
			  ,
			  new CourseInstanceWaitingList
			  {
			    // TODO: another test data instance
			  }
			}
			var mockUnitOfWork = new MockUnitOfWork();
			mockUnitOfWork.SetRepositoryData(waitingList);
			var systemUnderTest = new CoursesServiceProvider(mockUnitOfWork);

			// Act:

			// Assert:
		}
	}
}

For small applications, the amount of test data is usually relatively small and manageable within a given test. However, as the application becomes larger, and the number of tables grows, it becomes more likely that a given test will require a large amount of test data. It is however essential to keep the unit tests small and understandable.

We can declare test data in several places:

  • Declare test data in the Arrange section in each test (as seen above). This has the benefit of making the test easier to read and therefore understand, since all data is local to the test. However, if this results in duplication of test data between tests, we might want to refactor.
  • Declare common test data in the [TestInitialize] method. This way, all the unit tests (in the given unit test class at least) will share a common set of test data. This means that common test data is declared only once, but might mean that the readability of each test could suffer.
  • Declare test data in external files. This could be useful for test data which are used across unit test classes, but should be used sparingly.

Assert section

When writing the Assert section, we can use the Assert class which is accessible in unit test classes. It has several methods to help checking that the method we were testing had the intended consequences. Check out the documentation for the methods AreEqual, AreNotEqual, IsNull, IsNotNull, IsTrue etc.

Exceptions in unit tests

When a unit test throws an exception, it usually means that it fails. However, in some cases, we want the unit test to throw an exception, i.e. when that is the expected behaviour. For instance, we might write a unit test which tests that given invalid input, our method does in fact throw an exception of a given type (in the case below, the class AppObjectNotFoundException which we assume is a part of the business logic.

Example:

		[TestMethod]
		[ExpectedException(typeof(AppObjectNotFoundException))]
		public void CoursesTestGetCourseDetailsWithInvalidCourseInstanceID()
		{
			// Arrange:
			const int courseInstanceID = 1337;

			// Act:
			_service.GetCourseDetails(courseInstanceID);
		}

In some cases, we might want even more granularity in what the exception looks like. In that case, we can use the attribute ExpectedExceptionWithMessage which is available in the example project in Week 04 (TODO!!!). This class ensures that the exception thrown is of a given type, and that the message in the exception is a given string:

		[TestMethod]
		[ExpectedExceptionWithMessage(typeof(AppObjectNotFoundException), "INVALID_COURSEINSTANCE_ID")]
		public void CoursesTestGetCourseDetailsWithInvalidCourseInstanceID()
		{
			// Arrange:
			const int courseInstanceID = 1337;

			// Act:
			_service.GetCourseDetails(courseInstanceID);
		}

Auto test generation

Writing unit test data by hand can be OK for small applications, but say we want to write a unit test which tests what happens when we've got a list of 1000 items to work with. We could of course create a loop to generate this type of test data, but other options are available.

NBuilder has a nice API which allows us to generate test data in a more fluent way. For instance, this code generates a list of 10 courses:

	var courseList = Builder<Course>.CreateListOfSize(10)
		.TheFirst(6).With(x => x.Semester = "20143")
		.TheLast(4).With(y => y.Semester = "20131")
		.Build().ToList();

For/against

Not everyone is a big fan of unit tests. This article (pdf) has some points against unit testing. Here is a reply to that article. In general, you could say that unit testing is a useful tool, but just like other tools it should not be overused.