Files
DP44/GLM5Analysis/PromptTemplates/AddUnitTest.md
2026-04-17 14:55:32 -04:00

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_WhenPassedNull
  • ParseSensorName_ShouldReturnEmpty_WhenPassedEmptyString
  • ParseSensorName_ShouldReturnCorrectTag_WhenPassedValidName
  • CalculateTotal_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

  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