11 KiB
11 KiB
Add Unit Test - DataPRO Prompt Template
Context
DataPRO uses NUnit for unit testing with test projects in Common/DTS.Common.Tests/. Tests follow the Arrange-Act-Assert (AAA) pattern and use TestCaseSource for parameterized tests. The testing framework emphasizes clear test naming and comprehensive coverage of edge cases.
System Architecture
Common/DTS.Common.Tests/
├── ChannelTypeUtilityShould.cs # Example test file
├── FilterClassShould.cs
├── GroupChannelShould.cs
├── LinearizationFormulaShould.cs
├── NetworkUtilsShould.cs
├── DTS.Common.Tests.csproj
└── Properties/
└── AssemblyInfo.cs
Step-by-Step Instructions
1. Create Test Class
File: Common/DTS.Common.Tests/{ClassName}Should.cs
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace DTS.Common.Tests
{
[TestFixture]
public class {CLASS_NAME}Should
{
private {CLASS_NAME} _sut; // System Under Test
[SetUp]
public void Setup()
{
_sut = new {CLASS_NAME}();
}
[TearDown]
public void TearDown()
{
// Cleanup if needed
_sut = null;
}
[Test]
public void MethodName_ShouldReturnExpectedResult_WhenGivenValidInput()
{
// Arrange
var input = "valid input";
var expected = "expected output";
// Act
var result = _sut.MethodName(input);
// Assert
Assert.That(result, Is.EqualTo(expected));
}
}
}
2. Basic Test Patterns
Simple Assertion Test
[Test]
public void CalculateTotal_ShouldReturnSum_WhenGivenValidNumbers()
{
// Arrange
var calculator = new Calculator();
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var expected = 15;
// Act
var result = calculator.CalculateTotal(numbers);
// Assert
Assert.That(result, Is.EqualTo(expected));
}
Null/Empty Input Test
[Test]
public void ParseInput_ShouldReturnEmpty_WhenPassedNull()
{
// Arrange
// Act
var result = _sut.ParseInput(null);
// Assert
Assert.IsNotNull(result);
Assert.That(result, Is.EqualTo(string.Empty));
}
[Test]
public void ParseInput_ShouldReturnEmpty_WhenPassedEmptyString()
{
// Arrange
// Act
var result = _sut.ParseInput("");
// Assert
Assert.IsNotNull(result);
Assert.That(result, Is.EqualTo(string.Empty));
}
Exception Test
[Test]
public void Divide_ShouldThrowDivideByZeroException_WhenDivisorIsZero()
{
// Arrange
var dividend = 10;
var divisor = 0;
// Act & Assert
Assert.Throws<DivideByZeroException>(() => _sut.Divide(dividend, divisor));
}
[Test]
public void SetName_ShouldThrowArgumentException_WhenNameIsNull()
{
// Arrange
string name = null;
// Act & Assert
Assert.Throws<ArgumentException>(() => _sut.SetName(name));
}
3. Parameterized Tests
Using TestCase
[TestCase(1, 2, 3)]
[TestCase(10, 20, 30)]
[TestCase(-5, 5, 0)]
[TestCase(0, 0, 0)]
public void Add_ShouldReturnCorrectSum_WhenGivenTwoNumbers(int a, int b, int expected)
{
// Arrange
// Act
var result = _sut.Add(a, b);
// Assert
Assert.That(result, Is.EqualTo(expected));
}
Using TestCaseSource
public static IEnumerable<TestCaseData> ChannelTypeTestCases
{
get
{
yield return new TestCaseData("AC087155.1", "AC");
yield return new TestCaseData("DC123456.2", "DC");
yield return new TestCaseData("TM987654.3", "TM");
}
}
[TestCaseSource(nameof(ChannelTypeTestCases))]
public void ParseChannelType_ShouldReturnCorrectType_WhenGivenValidName(
string sensorName, string expectedType)
{
// Arrange
// Act
var result = _sut.ParseChannelType(sensorName);
// Assert
Assert.That(result, Is.EqualTo(expectedType));
}
Generating Test Data from Enum
public static Array GetKnownChannelTypes()
{
var testValuesFromEnum = new List<string>();
var values = Enum.GetValues(typeof(KnownChannelTypes))
.Cast<KnownChannelTypes>()
.Select(x => x.ToString())
.ToArray();
foreach (var value in values)
{
testValuesFromEnum.Add($"{value}087155.1");
}
return testValuesFromEnum.ToArray();
}
[TestCaseSource("GetKnownChannelTypes")]
public void ParseSensorKnownChannelType_ShouldReturnCorrectTag_WhenPassedSensorNameWithCorrectPrefix(
string sensorName)
{
// Arrange
// Act
var result = ChannelTypeUtility.ParseSensorKnownChannelType(sensorName);
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(Enum.IsDefined(typeof(KnownChannelTypes), result));
}
4. Testing Async Methods
[Test]
public async Task LoadDataAsync_ShouldReturnData_WhenDataExists()
{
// Arrange
var expectedCount = 5;
// Act
var result = await _sut.LoadDataAsync();
// Assert
Assert.IsNotNull(result);
Assert.That(result.Count, Is.EqualTo(expectedCount));
}
[Test]
public void ProcessAsync_ShouldThrowException_WhenInputInvalid()
{
// Arrange
var invalidInput = "";
// Act & Assert
Assert.ThrowsAsync<ArgumentException>(async () =>
await _sut.ProcessAsync(invalidInput));
}
5. Testing Events
[Test]
public void ValueChanged_ShouldRaiseEvent_WhenValueIsSet()
{
// Arrange
var eventRaised = false;
_sut.ValueChanged += (sender, args) => eventRaised = true;
// Act
_sut.Value = 42;
// Assert
Assert.IsTrue(eventRaised, "ValueChanged event was not raised");
}
[Test]
public void PropertyChanged_ShouldBeRaised_WhenNameChanges()
{
// Arrange
var eventArgs = new List<string>();
_sut.PropertyChanged += (sender, e) => eventArgs.Add(e.PropertyName);
// Act
_sut.Name = "New Name";
// Assert
Assert.Contains("Name", eventArgs);
}
6. Testing Collections
[Test]
public void GetItems_ShouldReturnEmptyCollection_WhenNoItemsAdded()
{
// Arrange
// Act
var result = _sut.GetItems();
// Assert
Assert.IsNotNull(result);
Assert.That(result, Is.Empty);
}
[Test]
public void AddItem_ShouldIncreaseCount_WhenItemIsValid()
{
// Arrange
var item = new Item { Id = 1, Name = "Test" };
var initialCount = _sut.ItemCount;
// Act
_sut.AddItem(item);
// Assert
Assert.That(_sut.ItemCount, Is.EqualTo(initialCount + 1));
}
[Test]
public void GetItems_ShouldReturnItemsInOrder_WhenItemsAdded()
{
// Arrange
_sut.AddItem(new Item { Id = 3 });
_sut.AddItem(new Item { Id = 1 });
_sut.AddItem(new Item { Id = 2 });
// Act
var result = _sut.GetItems().ToList();
// Assert
Assert.That(result[0].Id, Is.EqualTo(1));
Assert.That(result[1].Id, Is.EqualTo(2));
Assert.That(result[2].Id, Is.EqualTo(3));
}
7. Mocking Dependencies (if needed)
using Moq;
[TestFixture]
public class SensorServiceShould
{
private Mock<ISensorRepository> _mockRepository;
private SensorService _sut;
[SetUp]
public void Setup()
{
_mockRepository = new Mock<ISensorRepository>();
_sut = new SensorService(_mockRepository.Object);
}
[Test]
public void GetSensor_ShouldReturnSensor_WhenSensorExists()
{
// Arrange
var sensorId = 123;
var expectedSensor = new Sensor { Id = sensorId, Name = "Test" };
_mockRepository.Setup(r => r.Find(sensorId)).Returns(expectedSensor);
// Act
var result = _sut.GetSensor(sensorId);
// Assert
Assert.That(result, Is.EqualTo(expectedSensor));
_mockRepository.Verify(r => r.Find(sensorId), Times.Once);
}
[Test]
public void SaveSensor_ShouldCallRepository_WhenSensorIsValid()
{
// Arrange
var sensor = new Sensor { Id = 1, Name = "Test" };
// Act
_sut.SaveSensor(sensor);
// Assert
_mockRepository.Verify(r => r.Save(sensor), Times.Once);
}
}
Test Naming Convention
Follow this pattern: MethodName_Scenario_ExpectedResult
Examples:
ParseSensorName_ShouldReturnNull_WhenPassedNullParseSensorName_ShouldReturnEmpty_WhenPassedEmptyStringParseSensorName_ShouldReturnCorrectTag_WhenPassedValidNameCalculateTotal_ShouldThrowException_WhenListIsNull
Common Assertions
// Equality
Assert.That(result, Is.EqualTo(expected));
Assert.That(result, Is.Not.EqualTo(wrongValue));
// Null checks
Assert.That(result, Is.Null);
Assert.That(result, Is.Not.Null);
// Boolean
Assert.That(result, Is.True);
Assert.That(result, Is.False);
// Collections
Assert.That(collection, Is.Empty);
Assert.That(collection, Is.Not.Empty);
Assert.That(collection, Has.Count.EqualTo(5));
Assert.That(collection, Does.Contain(item));
// Exceptions
Assert.Throws<ArgumentException>(() => method());
Assert.ThrowsAsync<InvalidOperationException>(async () => await method());
// String
Assert.That(result, Does.StartWith("prefix"));
Assert.That(result, Does.EndWith("suffix"));
Assert.That(result, Does.Contain("substring"));
Assert.That(result, Is.Empty);
// Range
Assert.That(value, Is.InRange(1, 10));
Assert.That(value, Is.GreaterThan(0));
Assert.That(value, Is.LessThan(100));
// Type checking
Assert.That(result, Is.TypeOf<ExpectedType>());
Assert.That(result, Is.InstanceOf<IBaseInterface>());
Files to Create Summary
| File | Action |
|---|---|
DTS.Common.Tests/{ClassName}Should.cs |
Create |
Validation Checklist
- Test class has
[TestFixture]attribute - Test methods have
[Test]attribute [SetUp]used for test initialization[TearDown]used for cleanup (if needed)- Test names follow naming convention
- Each test has Arrange-Act-Assert sections
- Edge cases tested (null, empty, boundary values)
- Exception cases tested
- Test is isolated (doesn't depend on other tests)
- No external dependencies (use mocks/stubs)
- Assertions are specific and meaningful
Running Tests
# Run all tests in project
dotnet test Common/DTS.Common.Tests/
# Run specific test class
dotnet test --filter "FullyQualifiedName~ChannelTypeUtilityShould"
# Run specific test method
dotnet test --filter "FullyQualifiedName~ChannelTypeUtilityShould.ParseSensorKnownChannelType_ShouldReturnEmpty_WhenPassedNull"
Best Practices
- One assertion per test (or logically related assertions)
- Test behavior, not implementation
- Use meaningful test names that describe the scenario
- Keep tests independent - no shared state between tests
- Test edge cases - null, empty, max values, boundary conditions
- Don't test private methods directly - test through public API
- Use parameterized tests for similar test cases with different data
- Mock external dependencies - database, file system, network