Полное покрытие приложений ввода-вывода на основе файлов в .NET C# с использованием модульных тестов

Полное покрытие приложений ввода-вывода на основе файлов в .NET C# с использованием модульных тестов

14 февраля 2023 г.

Узнайте, как разделить приложение на более мелкие модули, которые вы можете охватить на 100 %

Работая в разных компаниях-разработчиках программного обеспечения, мне не раз приходилось работать над приложением, которое в основном основано на операциях файла ввода-вывода.

Самая большая проблема, с которой столкнулась команда при работе с такими приложениями, заключается в том, что файловые операции ввода-вывода очень сложно охватить модульными тестами, автоматизацией сборки Bamboo и многими другими вещами.

Поэтому раз и навсегда я решил придумать лучший дизайн, который я мог придумать, чтобы преодолеть эти проблемы. Однако напомню, что ничто в программном обеспечении не является абсолютной истиной. Для каждого приложения вы должны пересмотреть свой дизайн, посмотреть, где он подходит, а где нет, и, наконец, вам нужно адаптироваться.

Теперь, как обычно, я приведу простой пример и покажу вам наилучшее из возможных решений.


Пример приложения

Image by Ahmed Tarek

Наше приложение настолько простое с точки зрения требований:

  1. Как видите, пользовательский интерфейс прост. Для простоты он реализован как проект Windows Forms.
  2. Файл данных, с которым работает приложение, представляет собой текстовый файл с расширением .zzz
  3. .
  4. Каждая запись в файле данных представлена ​​в виде {имя},{возраст},{профессия} следующим образом: n Мохамед, 20 лет, бухгалтер n Патрик, 26 лет, механик Инженер n Сара, 23 года, тестировщик ПО
  5. Обратите внимание, что записи разделяются символом новой строки rn.
  6. Нажмите кнопку Обзор, чтобы открыть файл .zzz. Путь к файлу появится в текстовом поле, доступном только для чтения, над кнопкой "Обзор".
  7. Нажмите кнопку Получить все, чтобы приложение прочитало эти данные из выбранного файла .zzz и представило их в текстовом поле Reach. в нижней части пользовательского интерфейса.
  8. Нажмите кнопку Добавить, чтобы приложение добавило жестко заданную запись в файл и обновило текстовое поле Reach в нижней части пользовательского интерфейса.
  9. Нажмите кнопку Удалить, чтобы приложение удалило последнюю запись в файле и обновило текстовое поле Reach в нижней части пользовательского интерфейса.
  10. Вот несколько снимков экрана, которые помогут вам получить общее представление

    n После нажатия кнопки

After clicking Get All

After clicking Add

After clicking Remove

Весь код можно найти в этом репозитории, чтобы вы могли легко следить за ним. сильный>


Photo by Mikael Seegen on Unsplash

Отказ от ответственности

  1. Некоторые рекомендации были исключены или проигнорированы, чтобы сосредоточить основное внимание на основной цели и рекомендациях этой статьи.
  2. В решение могут быть внесены некоторые усовершенствования, но вам останется реализовать их в качестве упражнения.
  3. Весь код можно найти в этом репозитории, чтобы вы могли легко следить за ним. ли>
  4. В том же репозитории также доступен образец файла данных, который можно найти здесь< /a>.

Photo by Mehdi on Unsplash, adjusted by Ahmed Tarek

Неверный код

Это может быть первое, что приходит вам в голову при попытке реализовать это приложение.

using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace IOAbstraction
{
    public partial class FrmMain : Form
    {
        public FrmMain()
        {
            InitializeComponent();
        }

        private void Btn_Browse_Click(object sender, EventArgs e)
        {
            if (Ofd_Browse.ShowDialog() == DialogResult.OK)
            {
                Txt_Path.Text = Ofd_Browse.FileName;
            }
        }

        private void GetAll()
        {
            Rtb_AllResults.Clear();

            var lines = File.ReadAllLines(Txt_Path.Text);

            var builder = new StringBuilder();

            foreach (var line in lines)
            {
                if (!string.IsNullOrEmpty(line) && line.Contains(","))
                {
                    var parts = line.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                    var name = parts[0];
                    var age = parts[1];
                    var profession = parts[2];
                    var message = $"Name: {name}, Age: {age}, Profession: {profession}.";

                    builder.AppendLine(message);
                }
            }

            Rtb_AllResults.Text = builder.ToString();
        }

        private void Btn_Get_Click(object sender, EventArgs e)
        {
            GetAll();
        }

        private void Btn_Add_Click(object sender, EventArgs e)
        {
            var line = Environment.NewLine + "Ahmed,36,Software Engineer";

            var text = TrimEndNewLine(File.ReadAllText(Txt_Path.Text)) + line;

            File.WriteAllText(Txt_Path.Text, text);

            GetAll();
        }

        private void Btn_Remove_Click(object sender, EventArgs e)
        {
            var lines = File.ReadAllLines(Txt_Path.Text);

            File.WriteAllLines(Txt_Path.Text, lines.Take(lines.Length - 1));

            GetAll();
        }

        private string TrimEndNewLine(string str)
        {
            var result = str;

            while (result.EndsWith(Environment.NewLine))
            {
                result = result.Substring(0, result.Length - Environment.NewLine.Length);
            }

            return result;
        }
    }
}

Здесь мы можем заметить, что весь код находится в одном месте:

  1. Логика работы (открытие, чтение содержимого и запись содержимого) с физическим файлом.
  2. Логика выполнения команд пользовательского интерфейса.
  3. Логика форматирования данных и обновления пользовательского интерфейса.

Это создает множество проблем, таких как:

  1. Слишком много обязанностей для одного класса.
  2. В зависимости от статических классов, таких как System.IO.File.
  3. Невозможно протестировать логику операций ввода-вывода, не попробовав логику пользовательского интерфейса.
  4. Невозможно протестировать логику пользовательского интерфейса, не попробовав логику операций ввода-вывода.
  5. Потребуется всегда иметь физические файлы данных, чтобы можно было покрыть код модульными тестами.
  6. Даже если вам удастся создать эти модульные тесты и связанные с ними физические файлы, эти файлы всегда будут требовать обслуживания, хранения и т. д.
  7. И они превратят планирование и реализацию непрерывной интеграции (CI) и непрерывной доставки/развертывания (CD) в кошмар.

Поэтому пришло время исправить это.


Photo by Carson Masterson on Unsplash, adjusted by Ahmed Tarek

Хороший код

Основная идея здесь состоит в том, чтобы разделить все решение на более мелкие части, которые мы можем контролировать и легко покрыть модульными тестами.

ISystemFileOperationsManager

Здесь мы можем заметить следующее:

  1. Это интерфейс, представляющий некоторые файловые операции ввода-вывода, которые мы используем во всем нашем решении.
  2. Основная цель использования этого интерфейса – абстрагирование нашей зависимости от файловых операций ввода-вывода.
  3. Эта абстракция была бы очень полезна при попытке покрыть наше решение модульными тестами, поскольку теперь у нас есть определенная зависимость, которую мы можем имитировать.

NtfsOperationsManager

Здесь мы можем заметить следующее:

  1. Это реализация интерфейса ISystemFileOperationsManager.
  2. Это тонкая оболочка для класса System.IO.File.
  3. Вот почему мы можем легко и безопасно исключить этот класс из охвата кода, поскольку мы фактически не охватываем встроенные классы .NET.

Репозиторий IDataFileRepository

Здесь мы можем заметить следующее:

  1. Это интерфейс, представляющий менеджер репозитория, который знает о существовании наших файлов данных и о том, как записывать и читать текст в них и из них.
  2. Эта абстракция была бы очень полезна при попытке покрыть наше решение модульными тестами, поскольку теперь у нас есть определенная зависимость, которую мы можем имитировать.

Репозиторий файлов данных

Здесь мы можем заметить следующее:

  1. Это реализация интерфейса IDataFileRepository.
  2. Он внутренне зависит от ISystemFileOperationsManager и использует его для выполнения файловых операций ввода-вывода.

Ввод данных

Здесь мы можем заметить следующее:

  1. Это объект данных, представляющий нашу сущность, которая сохраняется и извлекается из наших файлов данных и из них.
  2. Свойство Age здесь реализовано как строка для простоты.
  3. Кроме того, этот класс должен реализовать IEquatable<DataEntry>, чтобы упростить применение к нему операций сравнения. Я бы оставил эту часть для вас.

Трансформатор IData

Здесь мы можем заметить следующее:

  1. Это интерфейс, представляющий любой преобразователь, который умеет преобразовывать текст в наш DataEntry.
  2. Эта абстракция была бы очень полезна при попытке покрыть наше решение модульными тестами, поскольку теперь у нас есть определенная зависимость, которую мы можем имитировать.

Преобразователь данных

Здесь мы можем заметить следующее:

  1. Это реализация интерфейса IDataTransformer.
  2. Этот класс инкапсулирует все знания о нашем преобразовании данных между текстом и DataEntry.

Менеджер данных

Здесь мы можем заметить следующее:

  1. Это интерфейс, представляющий любого менеджера, способного управлять данными нашего приложения без каких-либо сведений о носителе, на котором эти данные сохранены.
  2. На этом уровне нет ссылки на Файл.

Диспетчер данных

Здесь мы можем заметить следующее:

  1. Это реализация интерфейса IDataManager.
  2. Он внутренне зависит от IDaFileRepository и использует его для сохранения и извлечения данных в файлах данных и из них.
  3. Кроме того, он внутренне зависит от IDataTransformer и использует его для выполнения необходимых преобразований.

n Главное приложение

Здесь мы можем заметить следующее:

  1. Это класс, который обрабатывает бизнес-логику, запускаемую через пользовательский интерфейс приложения.
  2. Я не абстрагировал этот класс как интерфейс, но вы наверняка сможете это сделать. Я оставлю это вам для реализации.

Главная часть

Здесь мы можем заметить следующее:

  1. Это основной класс формы.
  2. Он внутренне зависит от класса MainApplication и использует его для выполнения основной бизнес-логики приложения.

Photo by Testalize.me on Unsplash, adjusted by Ahmed Tarek

Время тестирования

Теперь пришло время попытаться покрыть наше решение модульными тестами. Здесь вы заметите, как легко было бы покрыть все наше решение модульными тестами.

Каждый модуль теперь спроектирован так, чтобы делать как можно меньше, и имеет четко определенные зависимости.

Итак, теперь давайте создадим наш проект модульных тестов. Я использую библиотеки NUnit и Moq для тестирования и имитации.

Тесты DataFileRepository

using System.Collections.Generic;
using System.Linq;
using IOAbstraction.SystemFileOperationsManager;
using Moq;
using NUnit.Framework;

namespace IOAbstraction.UnitTests
{
    [TestFixture]
    public class DataFileRepositoryTests
    {
        private const string DummyDataFilePath = "This is a dummy path for testing";

        private Mock<ISystemFileOperationsManager> m_SystemFileOperationsManagerMock;
        private DataFileRepository.DataFileRepository m_Sut;

        [SetUp]
        public void SetUp()
        {
            m_SystemFileOperationsManagerMock = new Mock<ISystemFileOperationsManager>();

            m_Sut = new DataFileRepository.DataFileRepository(m_SystemFileOperationsManagerMock.Object,
                DummyDataFilePath);
        }

        [TearDown]
        public void TearDown()
        {
            m_Sut = null;
            m_SystemFileOperationsManagerMock = null;
        }

        [Test]
        public void GetAllDataText_ShouldReturnAllData()
        {
            // Arrange
            var text = "This is the sample text";

            m_SystemFileOperationsManagerMock
                .Setup
                (
                    m => m.ReadAllText(It.Is<string>(p => p == DummyDataFilePath))
                )
                .Returns(text)
                .Verifiable();

            // Act
            var actual = m_Sut.GetAllDataText();

            // Assert
            m_SystemFileOperationsManagerMock
                .Verify
                (
                    m => m.ReadAllText(DummyDataFilePath)
                );

            Assert.AreEqual(text, actual);
        }

        [TestCase(
            "Mohamed,20,Accountant",
            "Ahmed,36,Software Engineer",
            "Mohamed,20,Accountant" + "rn" + "Ahmed,36,Software Engineer",
            TestName = "Test Case 01")]
        [TestCase(
            "Mohamed,20,Accountantrn",
            "Ahmed,36,Software Engineer",
            "Mohamed,20,Accountant" + "rn" + "Ahmed,36,Software Engineer",
            TestName = "Test Case 02")]
        public void AddNewDataEntryText_ShouldAddDataInCorrectFormat(string existingData, string input, string expected)
        {
            // Arrange
            m_SystemFileOperationsManagerMock
                .Setup
                (
                    m => m.ReadAllText(It.Is<string>(p => p == DummyDataFilePath))
                )
                .Returns(existingData)
                .Verifiable();

            m_SystemFileOperationsManagerMock
                .Setup
                (
                    m => m.WriteAllText
                    (
                        It.Is<string>(p => p == DummyDataFilePath),
                        It.Is<string>(p => p == expected)
                    )
                )
                .Verifiable();

            // Act
            m_Sut.AddNewDataEntryText(input);

            // Assert
            m_SystemFileOperationsManagerMock
                .Verify
                (
                    m => m.ReadAllText(DummyDataFilePath)
                );

            m_SystemFileOperationsManagerMock
                .Verify
                (
                    m => m.WriteAllText
                    (
                        DummyDataFilePath,
                        expected
                    )
                );
        }

        [Test]
        public void RemoveLastDataEntryText_ShouldRemoveTheLastLine()
        {
            // Arrange
            var lines = new[] { "Line 1", "Line 2", "Line 3" };
            var expected = new[] { "Line 1", "Line 2" };

            m_SystemFileOperationsManagerMock
                .Setup
                (
                    m => m.ReadAllLines(It.Is<string>(p => p == DummyDataFilePath))
                )
                .Returns(lines)
                .Verifiable();

            m_SystemFileOperationsManagerMock
                .Setup
                (
                    m => m.WriteAllLines
                    (
                        It.Is<string>(p => p == DummyDataFilePath),
                        It.Is<IEnumerable<string>>(
                            p => p.Count() == 2 &&
                                 p.ElementAt(0) == expected[0] &&
                                 p.ElementAt(1) == expected[1])
                    )
                )
                .Verifiable();

            // Act
            m_Sut.RemoveLastDataEntryText();

            // Assert
            m_SystemFileOperationsManagerMock
                .Verify
                (
                    m => m.ReadAllLines(DummyDataFilePath)
                );

            m_SystemFileOperationsManagerMock
                .Verify
                (
                    m => m.WriteAllLines
                    (
                        DummyDataFilePath,
                        It.Is<IEnumerable<string>>(
                            p => p.Count() == 2 &&
                                 p.ElementAt(0) == expected[0] &&
                                 p.ElementAt(1) == expected[1])
                    )
                );
        }
    }
}

Тесты DataManager

using System.Collections.Generic;
using System.Linq;
using IOAbstraction.DataFileRepository;
using IOAbstraction.DataManager.Model;
using IOAbstraction.DataTransformer;
using Moq;
using NUnit.Framework;

namespace IOAbstraction.UnitTests
{
    [TestFixture]
    public class DataManagerTests
    {
        private Mock<IDataFileRepository> m_DataFileRepositoryMock;
        private Mock<IDataTransformer> m_DataTransformerMock;
        private DataManager.DataManager m_Sut;

        [SetUp]
        public void SetUp()
        {
            m_DataFileRepositoryMock = new Mock<IDataFileRepository>();
            m_DataTransformerMock = new Mock<IDataTransformer>();
            m_Sut = new DataManager.DataManager(m_DataFileRepositoryMock.Object, m_DataTransformerMock.Object);
        }

        [TearDown]
        public void TearDown()
        {
            m_Sut = null;
            m_DataFileRepositoryMock = null;
            m_DataTransformerMock = null;
        }

        [Test]
        public void GetAllData_ShouldGetAllData()
        {
            // Arrange
            var allDataText = "Mohamed,20,AccountantrnPatrick,26,Mechanical Engineer";

            var allData = new List<DataEntry>
            {
                new DataEntry("Mohamed", "20", "Accountant"),
                new DataEntry("Patrick", "26", "Mechanical Engineer")
            };

            m_DataFileRepositoryMock
                .Setup
                (
                    m => m.GetAllDataText()
                )
                .Returns(allDataText)
                .Verifiable();

            m_DataTransformerMock
                .Setup
                (
                    m => m.CombinedTextToDataEntries(
                        It.Is<string>(p => p == allDataText)
                    )
                )
                .Returns(allData)
                .Verifiable();

            // Act
            var actual = m_Sut.GetAllData();

            // Assert
            m_DataFileRepositoryMock
                .Verify
                (
                    m => m.GetAllDataText()
                );

            m_DataTransformerMock
                .Verify
                (
                    m => m.CombinedTextToDataEntries(allDataText)
                );

            Assert.AreEqual(2, actual.Count());
            Assert.AreEqual("Mohamed", actual.ElementAt(0).Name);
            Assert.AreEqual("20", actual.ElementAt(0).Age);
            Assert.AreEqual("Accountant", actual.ElementAt(0).Profession);
            Assert.AreEqual("Patrick", actual.ElementAt(1).Name);
            Assert.AreEqual("26", actual.ElementAt(1).Age);
            Assert.AreEqual("Mechanical Engineer", actual.ElementAt(1).Profession);
        }

        [Test]
        public void AddNewDataEntry_ShouldAddNewDataEntry()
        {
            // Arrange
            var entry = new DataEntry("Mohamed", "20", "Accountant");
            var entryText = "Mohamed,20,Accountant";

            var allData = new List<DataEntry>
            {
                new DataEntry("Patrick", "26", "Mechanical Engineer")
            };

            m_DataTransformerMock
                .Setup
                (
                    m => m.DataEntryToText(
                        It.Is<DataEntry>(p => p == entry)
                    )
                )
                .Returns(entryText)
                .Verifiable();

            m_DataFileRepositoryMock
                .Setup
                (
                    m => m.AddNewDataEntryText
                    (
                        It.Is<string>(p => p == entryText)
                    )
                )
                .Verifiable();

            // Act
            m_Sut.AddNewDataEntry(entry);

            // Assert
            m_DataTransformerMock
                .Verify
                (
                    m => m.DataEntryToText(entry)
                );

            m_DataFileRepositoryMock
                .Verify
                (
                    m => m.AddNewDataEntryText(entryText)
                );
        }

        [Test]
        public void RemoveLastDataEntryText_RemoveLastDataEntry()
        {
            // Arrange
            m_DataFileRepositoryMock
                .Setup
                (
                    m => m.RemoveLastDataEntryText()
                )
                .Verifiable();

            // Act
            m_Sut.RemoveLastDataEntryText();

            // Assert
            m_DataFileRepositoryMock
                .Verify
                (
                    m => m.RemoveLastDataEntryText()
                );
        }
    }
}

Тесты DataTransformer

using System.Linq;
using IOAbstraction.DataManager.Model;
using NUnit.Framework;

namespace IOAbstraction.UnitTests
{
    [TestFixture]
    public class DataTransformerTests
    {
        private DataTransformer.DataTransformer m_Sut;

        [SetUp]
        public void SetUp()
        {
            m_Sut = new DataTransformer.DataTransformer();
        }

        [TearDown]
        public void TearDown()
        {
            m_Sut = null;
        }

        [Test]
        public void CombinedTextToDataEntries_ShouldConvertCombinedEntriesTextIntoDataEntries01()
        {
            // Arrange
            var combinedText = "Mohamed,20,AccountantrnPatrick,26,Mechanical Engineer";

            // Act
            var actual = m_Sut.CombinedTextToDataEntries(combinedText);

            // Assert
            Assert.AreEqual(2, actual.Count());
            Assert.AreEqual("Mohamed", actual.ElementAt(0).Name);
            Assert.AreEqual("20", actual.ElementAt(0).Age);
            Assert.AreEqual("Accountant", actual.ElementAt(0).Profession);
            Assert.AreEqual("Patrick", actual.ElementAt(1).Name);
            Assert.AreEqual("26", actual.ElementAt(1).Age);
            Assert.AreEqual("Mechanical Engineer", actual.ElementAt(1).Profession);
        }

        [Test]
        public void CombinedTextToDataEntries_ShouldConvertCombinedEntriesTextIntoDataEntries02()
        {
            // Arrange
            var combinedText = "Mohamed,20,Accountantrn";

            // Act
            var actual = m_Sut.CombinedTextToDataEntries(combinedText);

            // Assert
            Assert.AreEqual(1, actual.Count());
            Assert.AreEqual("Mohamed", actual.ElementAt(0).Name);
            Assert.AreEqual("20", actual.ElementAt(0).Age);
            Assert.AreEqual("Accountant", actual.ElementAt(0).Profession);
        }

        [Test]
        public void CombinedTextToDataEntries_ShouldConvertCombinedEntriesTextIntoDataEntries03()
        {
            // Arrange
            var combinedText = "Mohamed,20,AccountantrnrnPatrick,26,Mechanical Engineer";

            // Act
            var actual = m_Sut.CombinedTextToDataEntries(combinedText);

            // Assert
            Assert.AreEqual(2, actual.Count());
            Assert.AreEqual("Mohamed", actual.ElementAt(0).Name);
            Assert.AreEqual("20", actual.ElementAt(0).Age);
            Assert.AreEqual("Accountant", actual.ElementAt(0).Profession);
            Assert.AreEqual("Patrick", actual.ElementAt(1).Name);
            Assert.AreEqual("26", actual.ElementAt(1).Age);
            Assert.AreEqual("Mechanical Engineer", actual.ElementAt(1).Profession);
        }

        [Test]
        public void TextToDataEntry_ShouldConvertEntryTextToDataEntry01()
        {
            // Arrange
            var combinedText = "Mohamed,20,Accountant";

            // Act
            var actual = m_Sut.TextToDataEntry(combinedText);

            // Assert
            Assert.AreEqual("Mohamed", actual.Name);
            Assert.AreEqual("20", actual.Age);
            Assert.AreEqual("Accountant", actual.Profession);
        }

        [Test]
        public void TextToDataEntry_ShouldConvertEntryTextToDataEntry02()
        {
            // Arrange
            var combinedText = "";

            // Act
            var actual = m_Sut.TextToDataEntry(combinedText);

            // Assert
            Assert.IsNull(actual);
        }

        [Test]
        public void TextToDataEntry_ShouldConvertEntryTextToDataEntry03()
        {
            // Arrange
            var combinedText = "Mohamed20Accountant";

            // Act
            var actual = m_Sut.TextToDataEntry(combinedText);

            // Assert
            Assert.IsNull(actual);
        }

        [Test]
        public void DataEntryToText_ShouldConvertDataEntryToDataText()
        {
            // Arrange
            var entry = new DataEntry("Mohamed", "20", "Accountant");
            var expectedText = "Mohamed,20,Accountant";

            // Act
            var actual = m_Sut.DataEntryToText(entry);

            // Assert
            Assert.AreEqual(expectedText, actual);
        }
    }
}

Основные тесты приложения

using System.Collections.Generic;
using IOAbstraction.DataManager;
using IOAbstraction.DataManager.Model;
using Moq;
using NUnit.Framework;

namespace IOAbstraction.UnitTests
{
    [TestFixture]
    public class MainApplicationTests
    {
        private Mock<IDataManager> m_DataManagerMock;
        private MainApplication.MainApplication m_Sut;

        [SetUp]
        public void SetUp()
        {
            m_DataManagerMock = new Mock<IDataManager>();

            m_Sut = new MainApplication.MainApplication(m_DataManagerMock.Object);
        }

        [TearDown]
        public void TearDown()
        {
            m_Sut = null;
            m_DataManagerMock = null;
        }

        [Test]
        public void GetAllToPresentInUi_ShouldGetAllDataIntoTextFormatToPresentInUi()
        {
            // Arrange
            var entries = new List<DataEntry>
            {
                new DataEntry("Mohamed", "20", "Accountant"),
                new DataEntry("Patrick", "26", "Mechanical Engineer")
            };

            var expected = new string[]
            {
                "Name: Mohamed, Age: 20, Profession: Accountant",
                "Name: Patrick, Age: 26, Profession: Mechanical Engineer",
            };

            m_DataManagerMock
                .Setup
                (
                    m => m.GetAllData()
                )
                .Returns(entries)
                .Verifiable();

            // Act
            var actual = m_Sut.GetAllToPresentInUi();

            // Assert
            CollectionAssert.AreEqual(expected, actual);
        }

        [Test]
        public void Add_ShouldAddEntry()
        {
            // Arrange
            var entry = new DataEntry("Mohamed", "20", "Accountant");

            m_DataManagerMock
                .Setup
                (
                    m => m.AddNewDataEntry
                    (
                        It.Is<DataEntry>(p => p.Name == entry.Name && p.Age == entry.Age &&
                                              p.Profession == entry.Profession)
                    )
                )
                .Verifiable();

            // Act
            m_Sut.Add(entry);

            // Assert
            m_DataManagerMock
                .Verify
                (
                    m => m.AddNewDataEntry
                    (
                        It.Is<DataEntry>(p => p.Name == entry.Name && p.Age == entry.Age &&
                                              p.Profession == entry.Profession)
                    )
                );
        }

        [Test]
        public void Remove_ShouldRemoveLastEntry()
        {
            // Arrange

            m_DataManagerMock
                .Setup
                (
                    m => m.RemoveLastDataEntryText()
                )
                .Verifiable();

            // Act
            m_Sut.Remove();

            // Assert
            m_DataManagerMock
                .Verify
                (
                    m => m.RemoveLastDataEntryText()
                );
        }
    }
}

Когда мы запустим все эти модульные тесты и рассчитаем покрытие тестами, мы получим вот такой результат.

Image by Ahmed Tarek

Как вы можете заметить на снимке экрана, единственной отсутствующей частью покрытия является сам код Form. Можно ли это прикрыть?

Да, это тоже можно было бы охватить, однако я оставлю это на ваше усмотрение.


Photo by nck_gsl on Pixabay, adjusted by Ahmed Tarek

Заключительные мысли

Теперь, благодаря новому дизайну, мы можем легко покрыть каждый аспект нашего решения модульными тестами, и нам очень легко получить полный контроль над модулями нашего приложения. Вот и все…

Наконец, надеюсь, вам было так же интересно читать эту статью, как мне было интересно ее писать.


Также опубликовано Здесь


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE