Полное покрытие приложений ввода-вывода на основе файлов в .NET C# с использованием модульных тестов
14 февраля 2023 г.Узнайте, как разделить приложение на более мелкие модули, которые вы можете охватить на 100 %
Работая в разных компаниях-разработчиках программного обеспечения, мне не раз приходилось работать над приложением, которое в основном основано на операциях файла ввода-вывода.
Самая большая проблема, с которой столкнулась команда при работе с такими приложениями, заключается в том, что файловые операции ввода-вывода очень сложно охватить модульными тестами, автоматизацией сборки Bamboo и многими другими вещами.
Поэтому раз и навсегда я решил придумать лучший дизайн, который я мог придумать, чтобы преодолеть эти проблемы. Однако напомню, что ничто в программном обеспечении не является абсолютной истиной. Для каждого приложения вы должны пересмотреть свой дизайн, посмотреть, где он подходит, а где нет, и, наконец, вам нужно адаптироваться.
Теперь, как обычно, я приведу простой пример и покажу вам наилучшее из возможных решений.
Пример приложения
Наше приложение настолько простое с точки зрения требований:
- Как видите, пользовательский интерфейс прост. Для простоты он реализован как проект Windows Forms.
- Файл данных, с которым работает приложение, представляет собой текстовый файл с расширением
.zzz
.
- Каждая запись в файле данных представлена в виде {имя},{возраст},{профессия} следующим образом: n
Мохамед, 20 лет, бухгалтер
nПатрик, 26 лет, механик Инженер
nСара, 23 года, тестировщик ПО
- Обратите внимание, что записи разделяются символом новой строки
rn
. - Нажмите кнопку Обзор, чтобы открыть файл
.zzz
. Путь к файлу появится в текстовом поле, доступном только для чтения, над кнопкой "Обзор". - Нажмите кнопку Получить все, чтобы приложение прочитало эти данные из выбранного файла
.zzz
и представило их в текстовом поле Reach. в нижней части пользовательского интерфейса. - Нажмите кнопку Добавить, чтобы приложение добавило жестко заданную запись в файл и обновило текстовое поле Reach в нижней части пользовательского интерфейса.
- Нажмите кнопку Удалить, чтобы приложение удалило последнюю запись в файле и обновило текстовое поле Reach в нижней части пользовательского интерфейса.
-
Вот несколько снимков экрана, которые помогут вам получить общее представление
n
Весь код можно найти в этом репозитории, чтобы вы могли легко следить за ним. сильный>
Отказ от ответственности
- Некоторые рекомендации были исключены или проигнорированы, чтобы сосредоточить основное внимание на основной цели и рекомендациях этой статьи.
- В решение могут быть внесены некоторые усовершенствования, но вам останется реализовать их в качестве упражнения.
- Весь код можно найти в этом репозитории, чтобы вы могли легко следить за ним. ли>
- В том же репозитории также доступен образец файла данных, который можно найти здесь< /a>.
Неверный код
Это может быть первое, что приходит вам в голову при попытке реализовать это приложение.
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;
}
}
}
Здесь мы можем заметить, что весь код находится в одном месте:
- Логика работы (открытие, чтение содержимого и запись содержимого) с физическим файлом.
- Логика выполнения команд пользовательского интерфейса.
- Логика форматирования данных и обновления пользовательского интерфейса.
Это создает множество проблем, таких как:
- Слишком много обязанностей для одного класса.
- В зависимости от статических классов, таких как
System.IO.File
. - Невозможно протестировать логику операций ввода-вывода, не попробовав логику пользовательского интерфейса.
- Невозможно протестировать логику пользовательского интерфейса, не попробовав логику операций ввода-вывода.
- Потребуется всегда иметь физические файлы данных, чтобы можно было покрыть код модульными тестами.
- Даже если вам удастся создать эти модульные тесты и связанные с ними физические файлы, эти файлы всегда будут требовать обслуживания, хранения и т. д.
- И они превратят планирование и реализацию непрерывной интеграции (CI) и непрерывной доставки/развертывания (CD) в кошмар.
Поэтому пришло время исправить это.
Хороший код
Основная идея здесь состоит в том, чтобы разделить все решение на более мелкие части, которые мы можем контролировать и легко покрыть модульными тестами.
ISystemFileOperationsManager
using System.Collections.Generic;
namespace IOAbstraction.SystemFileOperationsManager
{
public interface ISystemFileOperationsManager
{
string[] ReadAllLines(string path);
string ReadAllText(string path);
void WriteAllText(string path, string contents);
void WriteAllLines(string path, IEnumerable<string> contents);
}
}
Здесь мы можем заметить следующее:
- Это интерфейс, представляющий некоторые файловые операции ввода-вывода, которые мы используем во всем нашем решении.
- Основная цель использования этого интерфейса – абстрагирование нашей зависимости от файловых операций ввода-вывода.
- Эта абстракция была бы очень полезна при попытке покрыть наше решение модульными тестами, поскольку теперь у нас есть определенная зависимость, которую мы можем имитировать.
NtfsOperationsManager
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
namespace IOAbstraction.SystemFileOperationsManager
{
[ExcludeFromCodeCoverage]
public class NtfsOperationsManager : ISystemFileOperationsManager
{
public string[] ReadAllLines(string path)
{
return File.ReadAllLines(path);
}
public string ReadAllText(string path)
{
return File.ReadAllText(path);
}
public void WriteAllLines(string path, IEnumerable<string> contents)
{
File.WriteAllLines(path, contents);
}
public void WriteAllText(string path, string contents)
{
File.WriteAllText(path, contents);
}
}
}
Здесь мы можем заметить следующее:
- Это реализация интерфейса
ISystemFileOperationsManager
. - Это тонкая оболочка для класса
System.IO.File
. - Вот почему мы можем легко и безопасно исключить этот класс из охвата кода, поскольку мы фактически не охватываем встроенные классы .NET.
Репозиторий IDataFileRepository
namespace IOAbstraction.DataFileRepository
{
public interface IDataFileRepository
{
string GetAllDataText();
void AddNewDataEntryText(string dataEntryLine);
void RemoveLastDataEntryText();
}
}
Здесь мы можем заметить следующее:
- Это интерфейс, представляющий менеджер репозитория, который знает о существовании наших файлов данных и о том, как записывать и читать текст в них и из них.
- Эта абстракция была бы очень полезна при попытке покрыть наше решение модульными тестами, поскольку теперь у нас есть определенная зависимость, которую мы можем имитировать.
Репозиторий файлов данных
using System;
using System.Linq;
using IOAbstraction.SystemFileOperationsManager;
namespace IOAbstraction.DataFileRepository
{
public class DataFileRepository : IDataFileRepository
{
private readonly ISystemFileOperationsManager m_SystemFileOperationsManager;
private readonly string m_DataFileFullPath;
public DataFileRepository(ISystemFileOperationsManager systemFileOperationsManager, string dataFileFullPath)
{
m_SystemFileOperationsManager = systemFileOperationsManager;
m_DataFileFullPath = dataFileFullPath;
}
public string GetAllDataText()
{
return m_SystemFileOperationsManager.ReadAllText(m_DataFileFullPath);
}
public void AddNewDataEntryText(string dataEntryLine)
{
var line = Environment.NewLine + dataEntryLine;
var text = TrimEndNewLine(m_SystemFileOperationsManager.ReadAllText(m_DataFileFullPath)) + line;
m_SystemFileOperationsManager.WriteAllText(m_DataFileFullPath, text);
}
public void RemoveLastDataEntryText()
{
var lines = m_SystemFileOperationsManager.ReadAllLines(m_DataFileFullPath);
m_SystemFileOperationsManager.WriteAllLines(m_DataFileFullPath, lines.Take(lines.Length - 1));
}
private string TrimEndNewLine(string str)
{
var result = str;
while (result.EndsWith(Environment.NewLine))
{
result = result.Substring(0, result.Length - Environment.NewLine.Length);
}
return result;
}
}
}
Здесь мы можем заметить следующее:
- Это реализация интерфейса
IDataFileRepository
. - Он внутренне зависит от
ISystemFileOperationsManager
и использует его для выполнения файловых операций ввода-вывода.
Ввод данных
namespace IOAbstraction.DataManager.Model
{
public class DataEntry
{
public string Name { get; }
public string Age { get; }
public string Profession { get; }
public DataEntry(string name, string age, string profession)
{
Name = name;
Age = age;
Profession = profession;
}
}
}
Здесь мы можем заметить следующее:
- Это объект данных, представляющий нашу сущность, которая сохраняется и извлекается из наших файлов данных и из них.
- Свойство
Age
здесь реализовано как строка для простоты. - Кроме того, этот класс должен реализовать
IEquatable<DataEntry>
, чтобы упростить применение к нему операций сравнения. Я бы оставил эту часть для вас.
Трансформатор IData
using System.Collections.Generic;
using IOAbstraction.DataManager.Model;
namespace IOAbstraction.DataTransformer
{
public interface IDataTransformer
{
IEnumerable<DataEntry> CombinedTextToDataEntries(string combinedText);
DataEntry TextToDataEntry(string text);
string DataEntryToText(DataEntry dataEntry);
}
}
Здесь мы можем заметить следующее:
- Это интерфейс, представляющий любой преобразователь, который умеет преобразовывать текст в наш
DataEntry
. - Эта абстракция была бы очень полезна при попытке покрыть наше решение модульными тестами, поскольку теперь у нас есть определенная зависимость, которую мы можем имитировать.
Преобразователь данных
using System;
using System.Collections.Generic;
using System.Linq;
using IOAbstraction.DataManager.Model;
namespace IOAbstraction.DataTransformer
{
public class DataTransformer : IDataTransformer
{
public IEnumerable<DataEntry> CombinedTextToDataEntries(string combinedText)
{
var result = new List<DataEntry>();
var lines = combinedText.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (!string.IsNullOrEmpty(line) && line.Contains(","))
{
result.Add(TextToDataEntry(line));
}
}
return result.Where(r => r != null);
}
public DataEntry TextToDataEntry(string text)
{
DataEntry result = null;
if (!string.IsNullOrEmpty(text) && text.Contains(","))
{
var parts = text.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
var name = parts[0];
var age = parts[1];
var profession = parts[2];
result = new DataEntry(name, age, profession);
}
return result;
}
public string DataEntryToText(DataEntry dataEntry)
{
return $"{dataEntry.Name},{dataEntry.Age},{dataEntry.Profession}";
}
}
}
Здесь мы можем заметить следующее:
- Это реализация интерфейса
IDataTransformer
. - Этот класс инкапсулирует все знания о нашем преобразовании данных между текстом и
DataEntry
.
Менеджер данных
using System.Collections.Generic;
using IOAbstraction.DataManager.Model;
namespace IOAbstraction.DataManager
{
public interface IDataManager
{
IEnumerable<DataEntry> GetAllData();
void AddNewDataEntry(DataEntry newEntry);
void RemoveLastDataEntryText();
}
}
Здесь мы можем заметить следующее:
- Это интерфейс, представляющий любого менеджера, способного управлять данными нашего приложения без каких-либо сведений о носителе, на котором эти данные сохранены.
- На этом уровне нет ссылки на Файл.
Диспетчер данных
using System.Collections.Generic;
using IOAbstraction.DataFileRepository;
using IOAbstraction.DataManager.Model;
using IOAbstraction.DataTransformer;
namespace IOAbstraction.DataManager
{
public class DataManager : IDataManager
{
private readonly IDataFileRepository m_DataFileRepository;
private readonly IDataTransformer m_DataTransformer;
public DataManager(IDataFileRepository dataFileRepository, IDataTransformer dataTransformer)
{
m_DataFileRepository = dataFileRepository;
m_DataTransformer = dataTransformer;
}
public IEnumerable<DataEntry> GetAllData()
{
return m_DataTransformer.CombinedTextToDataEntries(m_DataFileRepository.GetAllDataText());
}
public void AddNewDataEntry(DataEntry newEntry)
{
m_DataFileRepository.AddNewDataEntryText(m_DataTransformer.DataEntryToText(newEntry));
}
public void RemoveLastDataEntryText()
{
m_DataFileRepository.RemoveLastDataEntryText();
}
}
}
Здесь мы можем заметить следующее:
- Это реализация интерфейса
IDataManager
. - Он внутренне зависит от
IDaFileRepository
и использует его для сохранения и извлечения данных в файлах данных и из них. - Кроме того, он внутренне зависит от
IDataTransformer
и использует его для выполнения необходимых преобразований.
n Главное приложение
using System.Collections.Generic;
using System.Linq;
using IOAbstraction.DataManager;
using IOAbstraction.DataManager.Model;
namespace IOAbstraction.MainApplication
{
public class MainApplication
{
private readonly IDataManager m_DataManager;
public MainApplication(IDataManager dataManager)
{
m_DataManager = dataManager;
}
public IEnumerable<string> GetAllToPresentInUi()
{
return m_DataManager
.GetAllData()
.Select(entry => $"Name: {entry.Name}, Age: {entry.Age}, Profession: {entry.Profession}")
.ToList();
}
public void Add(DataEntry entry)
{
m_DataManager.AddNewDataEntry(entry);
}
public void Remove()
{
m_DataManager.RemoveLastDataEntryText();
}
}
}
Здесь мы можем заметить следующее:
- Это класс, который обрабатывает бизнес-логику, запускаемую через пользовательский интерфейс приложения.
- Я не абстрагировал этот класс как интерфейс, но вы наверняка сможете это сделать. Я оставлю это вам для реализации.
Главная часть
using System;
using System.Windows.Forms;
using IOAbstraction.DataManager.Model;
using IOAbstraction.SystemFileOperationsManager;
namespace IOAbstraction
{
public partial class FrmMain : Form
{
private MainApplication.MainApplication m_MainApplication;
public FrmMain()
{
InitializeComponent();
}
private void Btn_Browse_Click(object sender, EventArgs e)
{
if (Ofd_Browse.ShowDialog() == DialogResult.OK)
{
Txt_Path.Text = Ofd_Browse.FileName;
var ntfsOperationsManager = new NtfsOperationsManager();
var dataTransformer = new DataTransformer.DataTransformer();
var dataFileRepository =
new DataFileRepository.DataFileRepository(ntfsOperationsManager, Txt_Path.Text);
var dataManager = new DataManager.DataManager(dataFileRepository, dataTransformer);
m_MainApplication = new MainApplication.MainApplication(dataManager);
}
}
private void GetAll()
{
Rtb_AllResults.Clear();
var lines = m_MainApplication.GetAllToPresentInUi();
Rtb_AllResults.Text = String.Join(Environment.NewLine, lines);
}
private void Btn_Get_Click(object sender, EventArgs e)
{
GetAll();
}
private void Btn_Add_Click(object sender, EventArgs e)
{
m_MainApplication.Add(new DataEntry("Ahmed", "36", "Software Engineer"));
GetAll();
}
private void Btn_Remove_Click(object sender, EventArgs e)
{
m_MainApplication.Remove();
GetAll();
}
}
}
Здесь мы можем заметить следующее:
- Это основной класс формы.
- Он внутренне зависит от класса
MainApplication
и использует его для выполнения основной бизнес-логики приложения.
Время тестирования
Теперь пришло время попытаться покрыть наше решение модульными тестами. Здесь вы заметите, как легко было бы покрыть все наше решение модульными тестами.
Каждый модуль теперь спроектирован так, чтобы делать как можно меньше, и имеет четко определенные зависимости.
Итак, теперь давайте создадим наш проект модульных тестов. Я использую библиотеки 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()
);
}
}
}
Когда мы запустим все эти модульные тесты и рассчитаем покрытие тестами, мы получим вот такой результат.
Как вы можете заметить на снимке экрана, единственной отсутствующей частью покрытия является сам код Form
. Можно ли это прикрыть?
Да, это тоже можно было бы охватить, однако я оставлю это на ваше усмотрение.
Заключительные мысли
Теперь, благодаря новому дизайну, мы можем легко покрыть каждый аспект нашего решения модульными тестами, и нам очень легко получить полный контроль над модулями нашего приложения. Вот и все…
Наконец, надеюсь, вам было так же интересно читать эту статью, как мне было интересно ее писать.
Также опубликовано Здесь
Оригинал