# 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 { 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(() => _sut.Divide(dividend, divisor)); } [Test] public void SetName_ShouldThrowArgumentException_WhenNameIsNull() { // Arrange string name = null; // Act & Assert Assert.Throws(() => _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 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(); var values = Enum.GetValues(typeof(KnownChannelTypes)) .Cast() .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(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(); _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 _mockRepository; private SensorService _sut; [SetUp] public void Setup() { _mockRepository = new Mock(); _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(() => method()); Assert.ThrowsAsync(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()); Assert.That(result, Is.InstanceOf()); ``` ## 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