Prompt file imported from uladzislauboika/FreeBelarus (
.github/prompts/add-unit-test.prompt.md). Copyright stays with the author.
Unit Test Implementation Guide
This prompt provides comprehensive examples and patterns for implementing unit tests following the FreeBelarus project guidelines.
Test Structure (AAA Pattern)
Always organize tests using the Arrange-Act-Assert pattern with clear comments:
[Fact]
public async Task MethodName_Scenario_ExpectedBehavior()
{
// Arrange
var mockDependency = new Mock<IDependency>();
mockDependency.Setup(x => x.Method()).ReturnsAsync(value);
var sut = new ServiceUnderTest(mockDependency.Object);
// Act
var result = await sut.MethodToTest();
// Assert
Assert.NotNull(result);
Assert.Equal(expectedValue, result.Property);
}
Mocking Examples
Correct Mocking Pattern
// ✅ CORRECT: Mock external dependencies
var mockRepository = new Mock<IEventRepository>();
var mockLogger = new Mock<ILogger<EventService>>();
var sut = new EventService(mockRepository.Object, mockLogger.Object);
// Setup method return values
mockRepository
.Setup(x => x.GetByIdAsync(It.IsAny<int>()))
.ReturnsAsync(new Event { Id = 1, Title = "Test" });
// ❌ INCORRECT: Don't mock the SUT
var mockService = new Mock<EventService>();
Complete Test Class Template
using FreeBelarus.Core.Entities;
using FreeBelarus.Core.Services;
using FreeBelarus.Data.Repositories;
using Moq;
using Xunit;
namespace FreeBelarus.Core.Tests.Services.Impl
{
public class EventServiceTests
{
private readonly Mock<IEventRepository> _mockEventRepository;
private readonly Mock<ICategoryRepository> _mockCategoryRepository;
private readonly EventService _sut;
public EventServiceTests()
{
// Setup common mocks in constructor
_mockEventRepository = new Mock<IEventRepository>();
_mockCategoryRepository = new Mock<ICategoryRepository>();
_sut = new EventService(_mockEventRepository.Object, _mockCategoryRepository.Object);
}
[Fact]
public async Task GetEventByIdAsync_WhenEventExists_ReturnsEvent()
{
// Arrange
var expectedEvent = CreateValidEvent(1, "Test Event");
_mockEventRepository
.Setup(x => x.GetByIdAsync(1))
.ReturnsAsync(expectedEvent);
// Act
var result = await _sut.GetEventByIdAsync(1);
// Assert
Assert.NotNull(result);
Assert.Equal(expectedEvent.Id, result.Id);
Assert.Equal(expectedEvent.Title, result.Title);
}
[Fact]
public async Task GetEventByIdAsync_WhenEventDoesNotExist_ReturnsNull()
{
// Arrange
_mockEventRepository
.Setup(x => x.GetByIdAsync(It.IsAny<int>()))
.ReturnsAsync((Event?)null);
// Act
var result = await _sut.GetEventByIdAsync(999);
// Assert
Assert.Null(result);
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(-100)]
public async Task GetEventByIdAsync_WithInvalidId_ThrowsArgumentException(int invalidId)
{
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(
() => _sut.GetEventByIdAsync(invalidId)
);
}
[Fact]
public async Task GetEventsAsync_WhenNoEvents_ReturnsEmptyCollection()
{
// Arrange
_mockEventRepository
.Setup(x => x.GetAllAsync())
.ReturnsAsync(new List<Event>());
// Act
var result = await _sut.GetEventsAsync();
// Assert
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
public async Task GetEventsAsync_WithCategoryFilter_ReturnsFilteredEvents()
{
// Arrange
var events = new List<Event>
{
CreateValidEvent(1, "Event 1"),
CreateValidEvent(2, "Event 2")
};
_mockEventRepository
.Setup(x => x.GetByCategoryAsync("history"))
.ReturnsAsync(events);
// Act
var result = await _sut.GetEventsAsync("history");
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Count());
}
[Fact]
public async Task CreateEventAsync_WithValidData_CreatesAndReturnsEvent()
{
// Arrange
var newEvent = CreateValidEvent(0, "New Event");
var createdEvent = CreateValidEvent(1, "New Event");
_mockEventRepository
.Setup(x => x.AddAsync(It.IsAny<Event>()))
.ReturnsAsync(createdEvent);
// Act
var result = await _sut.CreateEventAsync(newEvent);
// Assert
Assert.NotNull(result);
Assert.Equal(createdEvent.Id, result.Id);
_mockEventRepository.Verify(x => x.AddAsync(It.IsAny<Event>()), Times.Once);
}
[Fact]
public async Task CreateEventAsync_WithNullEvent_ThrowsArgumentNullException()
{
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(
() => _sut.CreateEventAsync(null!)
);
}
// Helper method for creating test data
private Event CreateValidEvent(int id, string title)
{
return new Event
{
Id = id,
Title = title,
Year = 2020,
ShortDescription = "Test Description",
DetailedDescription = "Test Detailed Description",
WhyImportant = "Test Importance",
HistoricalImpact = "Test Impact",
InterestingFact = "Test Fact",
Tags = new List<Tag>()
};
}
}
}
Theory Tests for Multiple Scenarios
Use [Theory] with [InlineData] for testing multiple similar scenarios:
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public async Task CreateEvent_WithInvalidTitle_ThrowsArgumentException(string title)
{
// Arrange
var eventData = new Event { Title = title };
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(
() => _sut.CreateEventAsync(eventData)
);
}
[Theory]
[InlineData(0, "Invalid ID: 0")]
[InlineData(-1, "Invalid ID: -1")]
[InlineData(-100, "Invalid ID: -100")]
public async Task GetEventByIdAsync_WithInvalidId_ThrowsArgumentException(int id, string scenario)
{
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentException>(
() => _sut.GetEventByIdAsync(id)
);
Assert.Contains("id", exception.Message);
}
Testing Async Methods
Always use async test methods when testing async code:
[Fact]
public async Task GetEventAsync_WhenCalled_ReturnsEvent()
{
// Arrange
var expectedEvent = new Event { Id = 1, Title = "Test Event" };
_mockRepository
.Setup(x => x.GetByIdAsync(1))
.ReturnsAsync(expectedEvent);
// Act
var result = await _sut.GetEventByIdAsync(1);
// Assert
Assert.Equal(expectedEvent.Id, result.Id);
Assert.Equal(expectedEvent.Title, result.Title);
}
[Fact]
public async Task GetAllEventsAsync_ReturnsAllEvents()
{
// Arrange
var expectedEvents = new List<Event>
{
new Event { Id = 1, Title = "Event 1" },
new Event { Id = 2, Title = "Event 2" }
};
_mockRepository
.Setup(x => x.GetAllAsync())
.ReturnsAsync(expectedEvents);
// Act
var result = await _sut.GetAllEventsAsync();
// Assert
Assert.Equal(2, result.Count());
Assert.Contains(result, e => e.Id == 1);
Assert.Contains(result, e => e.Id == 2);
}
Verifying Method Calls
Verify that dependencies were called correctly:
[Fact]
public async Task CreateEvent_WhenCalled_CallsRepositoryAdd()
{
// Arrange
var newEvent = new Event { Title = "New Event" };
var createdEvent = new Event { Id = 1, Title = "New Event" };
_mockRepository
.Setup(x => x.AddAsync(It.IsAny<Event>()))
.ReturnsAsync(createdEvent);
// Act
await _sut.CreateEventAsync(newEvent);
// Assert
_mockRepository.Verify(
x => x.AddAsync(It.Is<Event>(e => e.Title == "New Event")),
Times.Once
);
}
[Fact]
public async Task UpdateEvent_WhenCalled_CallsRepositoryUpdate()
{
// Arrange
var existingEvent = CreateValidEvent(1, "Original Title");
var updatedEvent = CreateValidEvent(1, "Updated Title");
_mockRepository
.Setup(x => x.GetByIdAsync(1))
.ReturnsAsync(existingEvent);
_mockRepository
.Setup(x => x.UpdateAsync(It.IsAny<Event>()))
.ReturnsAsync(updatedEvent);
// Act
await _sut.UpdateEventAsync(updatedEvent);
// Assert
_mockRepository.Verify(x => x.GetByIdAsync(1), Times.Once);
_mockRepository.Verify(
x => x.UpdateAsync(It.Is<Event>(e => e.Title == "Updated Title")),
Times.Once
);
}
Exception Testing
Test exception scenarios explicitly:
[Fact]
public async Task GetEventById_WithNullId_ThrowsArgumentNullException()
{
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => _sut.GetEventByIdAsync(null)
);
Assert.Equal("id", exception.ParamName);
}
[Fact]
public async Task CreateEvent_WithNullEvent_ThrowsArgumentNullException()
{
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => _sut.CreateEventAsync(null!)
);
Assert.Equal("eventEntity", exception.ParamName);
}
[Fact]
public async Task CreateEvent_WhenRepositoryFails_ThrowsInvalidOperationException()
{
// Arrange
var newEvent = CreateValidEvent(0, "Test Event");
_mockRepository
.Setup(x => x.AddAsync(It.IsAny<Event>()))
.ThrowsAsync(new InvalidOperationException("Database error"));
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => _sut.CreateEventAsync(newEvent)
);
Assert.Equal("Database error", exception.Message);
}
Test Data Builders
Create helper methods or builders for complex test data:
private Event CreateValidEvent(int id = 1, string title = "Test Event")
{
return new Event
{
Id = id,
Title = title,
Year = 2020,
ShortDescription = "Test Description",
DetailedDescription = "Test Detailed Description",
WhyImportant = "Test Importance",
HistoricalImpact = "Test Impact",
InterestingFact = "Test Fact",
Tags = new List<Tag>
{
new Tag { Id = 1, Name = "TestTag1" },
new Tag { Id = 2, Name = "TestTag2" }
}
};
}
private List<Event> CreateEventList(int count)
{
return Enumerable.Range(1, count)
.Select(i => CreateValidEvent(i, $"Event {i}"))
.ToList();
}
private Category CreateValidCategory(int id = 1, string name = "Test Category")
{
return new Category
{
Id = id,
Name = name,
Alias = name.Replace(" ", "").ToLower(),
StartYear = 2000,
EndYear = 2020
};
}
Testing Collections and Filtering
[Fact]
public async Task GetEventsByCategory_WithValidCategory_ReturnsFilteredEvents()
{
// Arrange
var categoryEvents = CreateEventList(3);
_mockRepository
.Setup(x => x.GetByCategoryAsync("history"))
.ReturnsAsync(categoryEvents);
// Act
var result = await _sut.GetEventsByCategoryAsync("history");
// Assert
Assert.NotNull(result);
Assert.Equal(3, result.Count());
Assert.All(result, evt => Assert.NotNull(evt.Title));
}
[Fact]
public async Task GetEventsByYear_WithValidYear_ReturnsFilteredEvents()
{
// Arrange
var yearEvents = new List<Event>
{
CreateValidEvent(1, "Event 1") { Year = 2020 },
CreateValidEvent(2, "Event 2") { Year = 2020 }
};
_mockRepository
.Setup(x => x.GetByYearAsync(2020))
.ReturnsAsync(yearEvents);
// Act
var result = await _sut.GetEventsByYearAsync(2020);
// Assert
Assert.Equal(2, result.Count());
Assert.All(result, evt => Assert.Equal(2020, evt.Year));
}
[Fact]
public async Task SearchEvents_WithValidKeyword_ReturnsMatchingEvents()
{
// Arrange
var searchResults = new List<Event>
{
CreateValidEvent(1, "Battle of Grunwald"),
CreateValidEvent(2, "Battle of Orsha")
};
_mockRepository
.Setup(x => x.SearchAsync("Battle"))
.ReturnsAsync(searchResults);
// Act
var result = await _sut.SearchEventsAsync("Battle");
// Assert
Assert.Equal(2, result.Count());
Assert.All(result, evt => Assert.Contains("Battle", evt.Title));
}
Advanced Mocking Scenarios
[Fact]
public async Task CreateEvent_WithTags_AssociatesTagsCorrectly()
{
// Arrange
var newEvent = CreateValidEvent(0, "New Event");
var existingTags = new List<Tag>
{
new Tag { Id = 1, Name = "TestTag1" },
new Tag { Id = 2, Name = "TestTag2" }
};
_mockEventRepository
.Setup(x => x.AddAsync(It.IsAny<Event>()))
.ReturnsAsync(newEvent);
_mockTagRepository
.Setup(x => x.GetByNamesAsync(It.IsAny<List<string>>()))
.ReturnsAsync(existingTags);
// Act
var result = await _sut.CreateEventWithTagsAsync(newEvent, new[] { "TestTag1", "TestTag2" });
// Assert
Assert.NotNull(result);
_mockTagRepository.Verify(
x => x.GetByNamesAsync(It.Is<List<string>>(tags =>
tags.Contains("TestTag1") && tags.Contains("TestTag2"))),
Times.Once
);
}
[Fact]
public async Task DeleteEvent_WhenEventExists_DeletesSuccessfully()
{
// Arrange
var existingEvent = CreateValidEvent(1, "Event to Delete");
_mockRepository
.Setup(x => x.GetByIdAsync(1))
.ReturnsAsync(existingEvent);
_mockRepository
.Setup(x => x.DeleteAsync(1))
.Returns(Task.CompletedTask);
// Act
await _sut.DeleteEventAsync(1);
// Assert
_mockRepository.Verify(x => x.GetByIdAsync(1), Times.Once);
_mockRepository.Verify(x => x.DeleteAsync(1), Times.Once);
}
[Fact]
public async Task DeleteEvent_WhenEventDoesNotExist_ThrowsNotFoundException()
{
// Arrange
_mockRepository
.Setup(x => x.GetByIdAsync(999))
.ReturnsAsync((Event?)null);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(
() => _sut.DeleteEventAsync(999)
);
_mockRepository.Verify(x => x.DeleteAsync(It.IsAny<int>()), Times.Never);
}
Integration with FluentAssertions (Optional)
If using FluentAssertions for more readable assertions:
using FluentAssertions;
[Fact]
public async Task GetEventByIdAsync_WhenEventExists_ReturnsEvent()
{
// Arrange
var expectedEvent = CreateValidEvent(1, "Test Event");
_mockRepository
.Setup(x => x.GetByIdAsync(1))
.ReturnsAsync(expectedEvent);
// Act
var result = await _sut.GetEventByIdAsync(1);
// Assert
result.Should().NotBeNull();
result.Id.Should().Be(expectedEvent.Id);
result.Title.Should().Be(expectedEvent.Title);
result.Tags.Should().HaveCount(2);
}
[Fact]
public async Task GetEventsAsync_ReturnsAllEvents()
{
// Arrange
var expectedEvents = CreateEventList(5);
_mockRepository
.Setup(x => x.GetAllAsync())
.ReturnsAsync(expectedEvents);
// Act
var result = await _sut.GetEventsAsync();
// Assert
result.Should().NotBeNull()
.And.HaveCount(5)
.And.OnlyContain(e => !string.IsNullOrEmpty(e.Title));
}
Summary
When generating unit tests:
- Always confirm the test project location first
- Use xUnit with Moq framework
- Follow AAA pattern with clear section comments
- Name tests descriptively using
MethodName_Scenario_ExpectedBehavior - Mock only external dependencies, never the SUT
- Cover happy path, edge cases, and exception scenarios
- Use Theory tests for multiple similar test cases
- Verify important method calls on mocked dependencies
- Create helper methods for test data generation
- Keep tests readable, maintainable, and focused on single scenarios