Optimization of the process of creating unit tests

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

    The fact is that I (as a programmer) am in a state of transition between the "classical" development and the development of test-driven, so I look for ways to simplify and make the latter more natural in every way. After a couple of squats, immediately get involved in the shai_xylyd techniquedid not succeed. He began correspondence with the author of the article, where he prompted me to think of a 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 components. Then draw conclusions.

    Theory


    First, a couple of definitions.

    The primary unit test is a block of code covering the “main” function of the entity being tested.
    Secondary unit test - a block of code that covers the "main" function of the tested entity in boundary conditions.

    Space Rpis a finite set of essential data in a programming environment.
    (In other words, this is the many existing instances of any data type of a fixed development platform).

    A function f(x) : Rp -> Rpis a sequence of code executed on data xfrom Rp.
    (An example - in a particular case, f(x)this is a simple method of a class that takes an input x. To put it even more crudely, then fthese are just lines of code).

    I need to define a primary unit test (hereinafter simply a test).

    Let z = h(x), where his the test function. We fix some value , then . Now we define a function that returns 0 (if incorrect) or 1 (if correct). In other words, we took some test data , performed some manipulations with it in the form and got it . Then we did for the received data and checked the correctness of the test. If you translate into pseudo-code, then the test will be the following sequence of pseudo-code: Let be a functional code (the one that will work in the entity being developed).xozo = h(xo)a(zo)zozoxoh(xo)zoassertzo


    def xo
    zo = h(xo)
    a(zo)


    f(x)
    According to the method described at the very beginning, I have to write functional code together with the test code. That is:,
    z = h(x) = f(m(x))where m(x)is the auxiliary code: stub objects of the dependencies of the functional code, mocha framework structures, etc. (hereinafter m(x)referred to as moki)

    Now there is a very important calculation: the nature of moki is such as to supply test data unchanged. Those. the essence of the mocha object is to replace the behavior of the dependency in the test by issuing a set of test data 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 in conjunction with the test code.

    Algorithm


    1. Definition of test data and verification of results

      def xo
      def zo
      a(zo)

    2. Functional Code Creation

      def xo
      zo = f(xo)
      a(zo)

    3. Creating moks and test helper code

      def xo
      zo = f(m(xo))
      a(zo)

    4. Refactoring - bringing f(x)to the entity under development

      def xo
      zo = h(xo)
      a(zo)



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

    Practice and examples


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

    The task is as follows. There is a topology of plants. It is necessary to determine the microservice, which returns the instance of the "Plant" class by the identifier of the plant:

    ///
    /// Сервис определения завода по идентификатору
    ///

    ///
    /// В случае, если в срезе данных нет таблиц с заводом
    ///

    public interface INodeResolver
    {
      ///
      /// Найти завод по идентификатору
      ///

      /// Идентификатор завода
      /// Завод
      Node FindById(int id);
    }

    * This source code was highlighted with Source Code Highlighter.


    The service is responsible for the topology data ITopologyService:

    ///
    /// Сервис данных топологии
    ///

    public interface ITopologyService
    {
      ///
      /// Возвращает "топологию" системы (БСУ, Контроллеры, БСО, Линии, etc)
      ///

      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 class instance using the passed identifier Node.

    Test creation



    Step 1. You need to determine 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. We determine the functionality of the entity being developed (obtain the object “plant” by the identifier of the 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();
      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();
      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 NodeResolveris as follows:

    ///
    /// Сервис определения завода
    ///

    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.


    conclusions


    Perhaps the most obvious advantage of the proposed method over the usual method of writing tests (when 4 is first executed and then implemented f(x)) is the time saving and development “smearing”. The programmer now does not have 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 one of two birds with one stone (refactoring itself is an o-small one that can be discarded).

    Thanks for attention.

    Also popular now: