461 lines
11 KiB
Markdown
461 lines
11 KiB
Markdown
|
|
# 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
|