📜 ⬆️ ⬇️

Unit tests in practice

Recently, quite a lot of publications on the topic of development through testing have appeared and continue to appear. The topic is quite interesting and worth it to devote to researching some of its time. We have been using unit testing in our team for the past year. In this article I want to talk about what happened and what kind of experience we have gained in the end.

At once I will make a reservation that examples are given with reference to the C # language and the .NET platform. Accordingly, in other languages ​​/ platforms, approaches and implementations may differ.

So…

What should be the unit tests?
')
Apart from the fact that unit tests must comply with the functionality of the software product, the main requirement is the speed of operation. If after the start of the test suite, the developer can take a break (in my case for a smoke break), then such launches will occur less and less (again, in my case, because of the fear of an overdose of nicotine). As a result, it may happen that the unit tests will not run at all and, as a result, the meaning of their writing will be lost. The programmer must be able to run the entire test suite at any time. And this set should execute as quickly as possible.

What requirements need to be met in order to ensure the speed of execution of unit tests?

Tests should be small

In the ideal case - one statement (assert) per test. The smaller the piece of functionality covered by the unit test, the faster the test will be performed.

By the way, on the theme of design. I really like the approach, which is formulated as “arrange-act-assert”.
Its essence is to clearly define preconditions in the unit test (initialization of test data,
presets), action (actually what is being tested) and postconditions (what should be in
the result of the action). This design improves the readability of the test and makes it easier.
use as documentation for the tested functionality.

If the development is using ReSharper from JetBrains, then it is very convenient to set up a template with which the blank for the test case will be created. For example, a template might look like this:

[Test] public void Test_$METHOD_NAME$() { //arrange $END$ //act //assert Assert.Fail("Not implemented"); } 


And then a test designed in this way may look something like this (all names are fictional, coincidences are random):

 [Test] public void Test_ForbiddenForPackageChunkWhenPackageNotFound() { //arrange var packagesRepositoryMock = _mocks.Create<IPackagesRepository>(); packagesRepositoryMock .Setup(r => r.FindPackageAsync(_packageId)) .Returns(Task<DatabasePackage>.Factory.StartNew(() => null)); Register(packagesRepositoryMock.Object); //act var message = PostChunkToServer(new byte[] { 1, 2, 3 }); //assert _mocks.VerifyAll(); Assert.That(message.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden)); } 


Tests must be isolated from the environment (database, network, file system)

This item is probably the most controversial of all. Often in the literature examples of using TDD in such tasks as the development of a calculator or telephone directory are considered. It is clear that these tasks are divorced from reality and the developer, returning to his project, does not know how to apply his knowledge and skills in his daily work. The simplest cases, similar to the notorious calculator, are covered by unit tests, and the rest of the functionality is developed on former rails. As a result, there is no understanding of why to spend time on unit tests, if simple code can be debugged and so, but complex still is not covered with tests.

In fact, there is nothing to worry about if a database or a file system is used in the unit tests. So, at least, you can be confident in the functionality of the functionality, which by and large is the core of the system. The only question is how to use the external environment in the most efficient way, keeping a balance between the isolation of tests and the speed of their execution?

Case 1. Data access layer (MS SQL Server)

If a MS SQL server is used in the development of a project, then the answer to this question may be to use an installed instance of MS SQL server (Express, Enterprise or Developer Edition) to deploy the test database. Such a database can be created using the standard mechanisms used in MS SQL Management Studio and put it into a project with unit tests. The general approach to using such a database is to deploy a test database before performing a test (for example, in the method marked with the SetUp attribute when using NUnit), filling the database with test data and checking the functionality of the repositories or gateways on these obviously known test data. Moreover, the test database can unfold both on a hard disk and in memory, using applications that create and manage a RAM disk. For example, in the project I'm working on at this time, the SoftPerfect RAM Disk application is used. The use of a RAM disk in unit tests allows to reduce the delays arising from I / O operations that would arise when a test database was deployed on a hard disk. Of course, this approach is not ideal, since it requires the introduction of third-party software into the developer’s environment. On the other hand, if we consider that the development environment is deployed, as a rule, once (well, or quite rarely), then this requirement does not seem to be so burdensome. And the gain from using this approach is quite tempting, because it is possible to monitor the correctness of one of the most important layers of the system.

By the way, if it is possible to use LINQ2SQL and SMO for MS SQL Server in module tests, then you can use the following base class to test the data access layer:

Code
 public abstract class DatabaseUnitTest<TContext> where TContext : DataContext { [TestFixtureSetUp] public void FixtureSetUp() { CreateFolderForTempDatabase(); } [SetUp] public void BeforeTestExecuting() { RestoreDatabaseFromOriginal(); RecreateContext(); } [TestFixtureTearDown] public void FixtureTearDown() { KillDatabase(); } protected string ConnectionString { get { return String.Format(@"Data Source={0};Initial Catalog={1};Integrated Security=True", TestServerName, TestDatabaseName); } } protected TContext Context { get; private set; } protected string TestDatabaseOriginalName { get { return "Database"; } } protected string ProjectName { get { return "CoolProject"; } } protected void RecreateContext() { Context = (TContext) Activator.CreateInstance(typeof(TContext), ConnectionString); } private string FolderForTempDatabase { get { return String.Format(@"R:\{0}.DatabaseTests\", ProjectName); } } private string TestDatabaseName { get { return FolderForTempDatabase + ProjectName + ".Tests"; } } private string TestDatabaseOriginalFileName { get { return Path.Combine(TestDatabaseDirectory, TestDatabaseOriginalName + ".mdf"); } } private string TestDatabaseFileName { get { return Path.Combine(TestDatabaseDirectory, TestDatabaseName + ".mdf"); } } private void CreateFolderForTempDatabase() { var directory = new DirectoryInfo(FolderForTempDatabase); if(!directory.Exists) { directory.Create(); } } private void RestoreDatabaseFromOriginal() { KillDatabase(); CopyFiles(); AttachDatabase(); } private void KillDatabase() { Server server = Server; SqlConnection.ClearAllPools(); if(server.Databases.Contains(TestDatabaseName)) { server.KillDatabase(TestDatabaseName); } } private void CopyFiles() { new FileInfo(TestDatabaseOriginalFileName).CopyTo(TestDatabaseFileName, true); string logFileName = GetLogFileName(TestDatabaseFileName); new FileInfo(GetLogFileName(TestDatabaseOriginalFileName)).CopyTo(logFileName, true); new FileInfo(TestDatabaseFileName).Attributes = FileAttributes.Normal; new FileInfo(logFileName).Attributes = FileAttributes.Normal; } private void AttachDatabase() { Server server = Server; if(!server.Databases.Contains(TestDatabaseName)) { server.AttachDatabase(TestDatabaseName, new StringCollection {TestDatabaseFileName, GetLogFileName(TestDatabaseFileName)}); } } private static string GetLogFileName(string databaseFileName) { return new Regex(".mdf$", RegexOptions.IgnoreCase).Replace(databaseFileName, "_log.ldf"); } private static Server Server { get { return new Server(TestServerName); } } private static string TestServerName { get { return "."; } } private static string TestDatabaseDirectory { get { var debugDirectory = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory); DirectoryInfo binDirectory = debugDirectory.Parent; DirectoryInfo testProjectDirectory; if(binDirectory == null || (testProjectDirectory = binDirectory.Parent) == null) { throw new Exception(""); } return Path.Combine(testProjectDirectory.FullName, "Database"); } } } 


After using which tests for interaction with the database will look like this:

 [TestFixture] public class ObjectFinderTest : DatabaseUnitTest<DatabaseDataContext> { [Test] public void Test_NullWhenObjectNotExists() { //arrange var fakeIdentifier = 0; var finder = new ObjectFinder(fakeIdentifier, ConnectionString); //act var foundObject = finder.Find(); //assert Assert.That(foundObject, Is.Null); } [Test] public void Test_SuccessfullyFound() { //arrange var insertedObject = ObjectsFactory.Create(); Context.Objects.InsertOnSubmit(insertedObject); Context.SubmitChanges(); var finder = new ObjectFinder(insertedObject.Id, ConnectionString); //act var foundObject = finder.Find(); //assert Assert.That(foundObject.Id, Is.EqualTo(insertedObject.Id)); Assert.That(foundObject.Property, Is.EqualTo(insertedObject.Property)); } } 


Voila! We got the opportunity to test the database access layer.

Case 2. ASP.NET MVC WebAPI

When testing WebAPI, one of the questions is how to build unit tests so that you can test calling the right methods of the desired controllers with the right arguments when sending a request to a specific url. If we assume that the controller’s responsibility is only to redirect the call to the appropriate class or component of the system, then the answer to the question about testing the controller will be to dynamically build some environment in which to start the necessary HTTP requests to the controller before running the tests. and, using mock'i, check the correctness of the configured routing. At the same time, I absolutely do not want to use it to deploy the IIS test environment. Ideally, a test environment should be created before running each test. This will help the unit tests to be fairly isolated from each other. With IIS in this regard, it would be quite difficult.

Fortunately, with the release of the .NET Framework 4.5, the opportunity to solve the problem of testing routing is quite simple. For example, using the following classes (Unity is used as the DI container):

Code
 public abstract class AbstractControllerTest<TController> where TController : ApiController { private HttpServer _server; private HttpClient _client; private UnityContainer _unityContainer; [SetUp] public void BeforeTestExecuting() { _unityContainer = new UnityContainer(); var configuration = new HttpConfiguration(); WebApiConfig.Register(configuration, new IoCContainer(_unityContainer)); _server = new HttpServer(configuration); _client = new HttpClient(_server); Register<TController>(); RegisterConstructorDependenciesAndInjectionProperties(typeof(TController)); } [TearDown] public void AfterTestExecuted() { _client.Dispose(); _server.Dispose(); _unityContainer.Dispose(); } protected TestHttpRequest CreateRequest(string url) { return new TestHttpRequest(_client, url); } protected void Register<T>(T instance) { Register(typeof(T), instance); } private void Register(Type type, object instance) { _unityContainer.RegisterInstance(type, instance); } private void Register<T>() { _unityContainer.RegisterType<T>(); } private void RegisterConstructorDependenciesAndInjectionProperties(Type controllerType) { var constructors = controllerType.GetConstructors(); var constructorParameters = constructors .Select(constructor => constructor.GetParameters()) .SelectMany(constructorParameters => constructorParameters); foreach (var constructorParameter in constructorParameters) { RegisterMockType(constructorParameter.ParameterType); } var injectionProperties = controllerType.GetProperties() .Where(info => info.GetCustomAttributes(typeof(DependencyAttribute), false) .Any()); foreach (var property in injectionProperties) { RegisterMockType(property.PropertyType); } } private void RegisterMockType(Type parameterType) { dynamic mock = Activator.CreateInstance(typeof(Mock<>).MakeGenericType(parameterType), new object[] { MockBehavior.Default }); Register(parameterType, mock.Object); } } 

 public sealed class TestHttpRequest { private readonly HttpClient _client; private readonly Uri _uri; public TestHttpRequest(HttpClient client, string url) { _client = client; _uri = new Uri(new Uri("http://can.be.anything/"), url); } public void AddHeader(string header, object value) { _client.DefaultRequestHeaders.Add(header, value.ToString()); } public HttpResponseMessage Get() { return _client.GetAsync(_uri).Result; } public HttpResponseMessage Post(byte[] content) { return _client.PostAsync(_uri, new ByteArrayContent(content)).Result; } public HttpResponseMessage Put(byte[] content) { return _client.PutAsync(_uri, new ByteArrayContent(content)).Result; } public HttpResponseMessage Head() { var message = new HttpRequestMessage(HttpMethod.Head, _uri); return _client.SendAsync(message).Result; } } 


Now you can use these classes to test a fictional controller that returns serialized dates and objects of a certain Platform class.

 [TestFixture] public class MyControllerTest : AbstractControllerTest<MyController> { private MockRepository _mocks; protected override void OnSetup() { _mocks = new MockRepository(MockBehavior.Strict); } [Test] public void Test_GetDates() { //arrange var january = new DateTime(2013, 1, 1); var february = new DateTime(2013, 2, 1); var repositoryMock = _mocks.Create<IRepository>(); repositoryMock .Setup(r => r.GetDates()) .Returns(new[] {january, february}); Register(repositoryMock.Object); //act var dates = ExecuteGetRequest<DateTime[]>("/api/build-dates"); //assert _mocks.VerifyAll(); Assert.That(dates, Is.EquivalentTo(new[] { january, february })); } [Test] public void Test_GetPlatforms() { //arrange var platform1 = new Platform {Id=1, Name = "1"}; var platform2 = new Platform {Id=2, Name = "2"}; var repositoryMock = _mocks.Create<IRepository>(); repositoryMock .Setup(r => r.GetPlatforms()) .Returns(new[] { platform1, platform2 }); Register(repositoryMock.Object); //act var platforms = ExecuteGetRequest<Platform[]>("/api/platforms"); //assert _mocks.VerifyAll(); Assert.That(platforms, Is.EquivalentTo(new[] { platform1, platform2 })); } private T ExecuteGetRequest<T>(string uri) { var request = CreateRequest(url); var response = request.Get(); T result; response.TryGetContentValue(out result); return result; } } 


That's all. Our controllers are covered with unit tests.

Case 3. Everything else

And with all the rest is quite simple. Examples of unit tests for classes that contain pure logic, without interacting with any external environment, are practically the same as those proposed in popular literature such as "TDD by Example" by Kent Beck. Therefore, there are no special tricks here.

I will add that in addition to reducing the number of errors in the program logic, you can also get the following benefits from the use of unit tests:


It is probably worth noting that the listed "buns" will always be fresh and tasty, while respecting the principle of "test first". Have the requirements changed? Add a test, change the code. Correct the error? Add a test, change the code. The most difficult thing is to change the perception of tests. Often, unit tests are perceived as something outsider, alien to the "main" code. This is, in my opinion, the main obstacle to using TDD in full. And it needs to be overcome, to realize that the unit tests and the programmed functionality are parts of one whole.

To date, the project, which our team is working on, has about 1000 unit tests. The build time and run all the tests on TeamCity is a little more than 4 minutes. The approaches described in the article allow us to test almost all layers of the system, controlling the change and development of the code. I hope that our experience will be useful for someone.

Source: https://habr.com/ru/post/191986/


All Articles