Files

461 lines
11 KiB
Markdown
Raw Permalink Normal View History

2026-04-17 14:55:32 -04:00
# 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`
```csharp
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
```csharp
[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
```csharp
[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
```csharp
[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
```csharp
[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
```csharp
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
```csharp
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
```csharp
[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
```csharp
[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
```csharp
[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)
```csharp
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_WhenPassedNull`
- `ParseSensorName_ShouldReturnEmpty_WhenPassedEmptyString`
- `ParseSensorName_ShouldReturnCorrectTag_WhenPassedValidName`
- `CalculateTotal_ShouldThrowException_WhenListIsNull`
## Common Assertions
```csharp
// 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
```bash
# 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
1. **One assertion per test** (or logically related assertions)
2. **Test behavior, not implementation**
3. **Use meaningful test names that describe the scenario**
4. **Keep tests independent** - no shared state between tests
5. **Test edge cases** - null, empty, max values, boundary conditions
6. **Don't test private methods directly** - test through public API
7. **Use parameterized tests for similar test cases with different data**
8. **Mock external dependencies** - database, file system, network