📜 ⬆️ ⬇️

Optimization of the process of creating unit tests

Hello! Habrayuzer shai_xylyd wrote an article about the aspects of testing, where they considered some of the concepts and values ​​of TDD. In particular, he mentioned a very interesting way to create primary unit tests - when a functional code is written in conjunction with a unit test code, which intrigued me a lot.

The fact is that I (as a programmer), am in a state of transition between “classical” development and development of test-driven, so I’m looking for ways to simplify and make the latter more natural. After a couple of squats, I was unable to immediately join the shai_xylyd technique. He began a correspondence with the author of the article, where he gave me the idea to approach the solution from a mathematical point of view. The idea is to take advantage of the functional space of the programming environment and “decompose” the writing of the unit test into its components. Then draw conclusions.

Theory


First, a couple of definitions.

The primary unit test is a block of code that covers the “main” function of the entity being tested.
A secondary unit test is a block of code that covers the “main” function of the tested entity in boundary conditions.
')
The space Rp is a finite set of essential data of the programming environment.
(In other words, this is the set of existing instances of any data type of a fixed development platform).

The function f(x) : Rp -> Rp is the name of a certain code sequence executed on the data x from Rp .
(An example is, in the particular case, f(x) is a simple class method that takes x as input. If it is even tougher, then f is just a line of code).

I need to determine the primary unit test (hereinafter just a test).

Let z = h(x) , where h is a test function. Fix some value x o , then z o = h(x o ) . Now we define the function a(z o ) , which returns 0 (if z o incorrect) or 1 (if z o correct). In other words, we took some test data x o , made some manipulations with them in the form of h(x o ) and got z o . Then we made an assert for the received data z o and checked the validity of the test.
If translated into pseudo-code, then the test will be the following sequence of pseudo-code:

def x o
z o = h(x o )
a(z o )


Let f(x) be a functional code (one that will work in the entity being developed).
According to the method described at the very beginning, I have to write functional code together with the test code. I.e:
z = h(x) = f(m(x)) , where m(x) is an auxiliary code: stub objects of the functional code dependencies, framework framework structures, etc. (further m(x) - moki)

Now a very important calculation: the nature of mocks is such as to deliver test data unchanged. Those. The essence of the mock object is to replace the dependency behavior in a test by producing a test data set defined by the programmer. In other words, m(x) = x . Hence the separation f(m(x)) = f(x) . The latter allows you to clearly describe the test creation algorithm, where the functional code is developed together with the test code.

Algorithm


  1. Test data definition and validation

    def x o
    def z o
    a(z o )

  2. Creation of functional code

    def x o
    z o = f(x o )
    a(z o )

  3. Creating mocks and auxiliary test code

    def x o
    z o = f(m(x o ))
    a(z o )

  4. Refactoring - putting f(x) into the entity being developed

    def x o
    z o = h(x o )
    a(z o )



At each stage, the test should run successfully. What is important, the TDD property is preserved - first we write a test for the entity, then the entity itself.

Practice and examples


I will try this method on cats myself. Platform - .net, language - C #, test pad - NUnit 2.x + Rhino Mocks 3.x

The task is as follows. There is a topology of plants. You need to define a microservice, which, by a plant identifier, returns an instance of the “Plant” class:

/// <summary>
///
/// </summary>
/// <remarks>
/// ,
/// </remarks>
public interface INodeResolver
{
/// <summary>
///
/// </summary>
/// <param name="id"> </param>
/// <returns></returns>
Node FindById( int id);
}


* This source code was highlighted with Source Code Highlighter .


ITopologyService is responsible for these topologies:

/// <summary>
///
/// </summary>
public interface ITopologyService
{
/// <summary>
/// "" (, , , , etc)
/// </summary>
DataSets.TopologyData GetTopology(IDataFilter filter);
}


* This source code was highlighted with Source Code Highlighter .


Those. I need to create a service that has a dependency on ITopologyService , receives data from it and creates a new instance of the Node class using the passed identifier.

Creating a test



Step 1. You need to define the test data and the resulting value.

[Test]
public void FindNodeByIdTest()
{
// x0
TopologyData data = new TopologyData();
data.Node.AddNodeRow(1, "1" , Guid .NewGuid());

// z0
Node node = new Node { Id = 1, Name = "1" };

Assert.AreEqual(1, node.Id);
Assert.AreEqual( "1" , node.Name);
}


* This source code was highlighted with Source Code Highlighter .


Step 2. Determine the functionality of the developed entity (by the plant identifier get the object "plant")

[Test]
public void FindNodeByIdTest2()
{
// x0
TopologyData data = new TopologyData();
data.Node.AddNodeRow(1, "1" , Guid .NewGuid());

// f(x0)
TopologyData.NodeRow nodeRow = data.Node.FindByID(1);

// z0
Node node = new Node { Id = 1, Name = nodeRow.Description };
Assert.AreEqual(1, node.Id);
Assert.AreEqual( "1" , node.Name);
}

* This source code was highlighted with Source Code Highlighter .


Step 3. Create moki

[Test]
public void FindNodeByIdTest3()
{
MockRepository repo = new MockRepository();

// x0
TopologyData data = new TopologyData();
data.Node.AddNodeRow(1, "1" , Guid .NewGuid());

// m(x0)
ITopologyService service = repo.StrictMock<ITopologyService>();
service.Expect(x => x.GetTopology(EmptyFilter.Instance)).Return(data).Repeat.Once();

repo.ReplayAll();

// f(m(x0)) = f(x0)
TopologyData dataSet = service.GetTopology(EmptyFilter.Instance);
TopologyData.NodeRow nodeRow = dataSet.Node.FindByID(1);

repo.VerifyAll();

// z0
Node node = new Node { Id = 1, Name = nodeRow.Description };
Assert.AreEqual(1, node.Id);
Assert.AreEqual( "1" , node.Name);
}


* This source code was highlighted with Source Code Highlighter .


Step 4: Refactoring and creating an entity:

[Test]
public void FindNodeByIdTest4()
{
MockRepository repo = new MockRepository();

// x0
TopologyData data = new TopologyData();
data.Node.AddNodeRow(1, "1" , Guid .NewGuid());

// m(x0)
ITopologyService service = repo.StrictMock<ITopologyService>();
service.Expect(x => x.GetTopology(EmptyFilter.Instance)).Return(data).Repeat.Once();

repo.ReplayAll();

NodeResolver resolver = new NodeResolver(service);
// z0
Node node = resolver.FindById(1);

repo.VerifyAll();

Assert.AreEqual(1, node.Id);
Assert.AreEqual( "1" , node.Name);
}


* This source code was highlighted with Source Code Highlighter .


The developed entity NodeResolver turned out like this:

/// <summary>
///
/// </summary>
public class NodeResolver : INodeResolver
{
public NodeResolver(ITopologyService topologyService)
{
Guard.ArgumentNotNull(topologyService, "service" );
_data = topologyService.GetTopology(EmptyFilter.Instance);
}

#region INodeResolver Members

public Node FindById( int id)
{
// f(x)
return new Node { Id = id, Name = _data.Node.FindByID(id).Description };
}

#endregion

private TopologyData _data;
}


* This source code was highlighted with Source Code Highlighter .


findings


Perhaps the most obvious advantage of the proposed method over the usual method of writing tests (when 4 is first executed and then f(x) is implemented) is the time saving and “smearing” of the development. The programmer no longer has to spend time on code that does not directly relate to the functionality of the program. He writes the code together with the test, making two rabbits one (the refactoring itself is o-small, which can be discarded).

Thanks for attention.

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


All Articles