Unit Test
Unit testing is a type of software testing where individual units or components of a software are tested in isolation. The purpose is to validate that each unit of the software performs as expected. A unit is the smallest testable part of an application, such as a function, method, or class.
Arrange - Act - Assert
Arrange-Act-Assert(AAA) is a simple and effective pattern for writing clear and structured unit tests: Arrange-set up the necessary objects and state, Act-perfom the action you want to test, Assert-verify that the action produced the expected result.
Commenting the AAA phases is recommended, especially during learning, in shared codebases, or for more complex test cases. It improves readability, consistency, and maintainability of tests.
Scope
The scope of unit tests typically focuses on classes or methods that contain business logic or make meaningful decisions. Simple data containers or trivial getters/setters usually don’t need unit tests unless they involve validation or side effects.
Naming Conventions
- Test Project Name: Add .Tests to your main project name, e.g. MyApp -> MyApp.Tests
- Test Namespace: Match the structure of the project being tested. e.g. MyApp.Services -> MyApp.Tests.Services
- Test Class Name: Name it after the class being tested, followed by Tests. e.g. UserService -> UserServiceTests
- Test Method Naming: MethodName_ExpectedResult_Condition, e.g. GetUserById_ReturnsUser_ValidId()
- The Tested Object Naming: sut(System Under Test), e.g. var sut = new UserService() *Actual vs Expected Naming: ActualResult, ExpectedResult
Integration Test
An Integration Test is a type of software test that verifies how different modules or components of an application work together. Unlike unit tests that test individual methods or classes in isolation, integration tests focus on the interaction between multiple parts-such as databases, APIs, services, or external systems.
The goal is to catch issues related to data flow, communication, or integration logic that may not be apparent in isolated unit testing.
End To End Test
An End-to-End Test is a type of software testing that verifies the entire application flow, from start to finish, to ensure that the system works as intended in a real-world scenario. E2E tests simulate user interactions with the application and check if the system components, such as the frontend, backend, and external dependencies, are functioning correctly together.
User Acceptance Test
A User Acceptance Test is the final phase of testing in software development where the actual users or clients verify if the software meets their business requirements and is ready for production. UAT focuses on ensuring that the system does what the end users expect it to do in a real-word context.
Test Driven Development(TDD)
Test Driven Development is a software development practice where development is guided by tests. In TDD, you iteratively write a test, implement just enough code to make the test pass, and then refactor the code-all within short, repeatable cycles. This process helps ensure that the system behaves as expected from the start.
What makes a good test?
A good test should be automated and repeatable, allowing it to be run frequently without manual intervention. It must be easy to run, with minimal setup, and should execute quickly, ensuring fast feedback during development. A reliable test is one that produces stable results- it pass or fails consistently under the same conditions. Good tests are also fully isolated from external dependencies, such as databases, file systems, or network calls, to avoid flakiness. Additionally, tests should be easy to read and understand, clearly expressing both the expected outcomes and the intent of the test. Finally, a trustworthy test suite provides high confidence: when tests fail, they point to real problems; when they pass, they strongly indicate that the code is working as intended.
Unit Test Framework(AutoFixture + Moq/FakeItEasy + xUnit + FluentAssertion)
Step1: Install Required NuGet Packages
// core test framework
dotnet add package xunit
// A mocking library to create fake dependencies
dotnet add package Moq
// Automatically generates anonymous test data
dotnet add package AutoFixture
// Integrates AutoFixture with Moq
dotnet add package AutoFixture.AutoMoq
// Integrates AutoFixture with xUnit’s [Theory]
dotnet add package AutoFixture.Xunit2
// provides readable and expressive assertions
dotnet add package FluentAssertions
Step2: Create AutoMoqAttribute
public class AutoMoqDataAttribute : AutoDataAttribute
{
public AutoMoqDataAttribute()
: base(() =>
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
// if some object may cause circular reference, we ask moq does not generate it for us, and we create it by ourself.
fixture.Customize<Category>(
c => c.Without(x => x.Tasks));
return fixture;
})
{
}
}
Step3: Write Unit Tests
public class TaskCategoryServiceTests
{
// AutoMoqData tell fixture uses moq creating mock object
[Theory, AutoMoqData]
public async Task CreateTaskAsync_ShouldReturnSuccess_WhenCategoryExists(
[Frozen] Mock<ICategoryRepository> mockCategoryRepository,
[Frozen] Mock<ITaskRepository> mockTaskRepository,
TaskCategoryService sut,
string title,
string description,
DateTime dueDate,
string userId,
string categoryId,
int priority,
Category category)
{
// Arrange
mockCategoryRepository
.Setup(repo => repo.GetCategoryByIdAsync(categoryId, userId))
.ReturnsAsync(category);
// Act
var actualResult = await sut
.CreateTaskAsync(title, description, dueDate, priority, userId, categoryId);
// Assert
// fluent Assertion
actualResult.Should().Be(ApiResponseCode.TaskCreateSuccess);
mockTaskRepository.Verify(r => r.AddTaskAsync(It.IsAny<TaskItem>()), Times.Once);
}
1.Why does Moq fail to mock some methods?
Answer:
Moq can only mock interfaces, virtual, or abstract members of non-sealed classes. If you're mocking a concrete class with non-virtual methods, Moq can't intercept the calls, so Setup(...).Returns(...)
won't work.
2.Why does mock.Setup(...).ReturnsAsync(...)
not match, even when the values seem equal?
Answer:
Because you're passing a different instance of the object. Moq compares by reference, not by value. Use It.Is<Category>(c => ...)
to match by property values instead, or It.IsAny<Category>()
if you don't care about the specifics.
3.How can I simulate assigning an ID during testing (like a database would)?
Answer:
Use .Callback<Category>(c => c.Id = categoryId)
in your setup to manually assign the ID inside the mocked method. This simulates the database behavior and allows your test object to have the expected Id
.
4.Mocking UserManager<TUser>
with AutoFixture and Moq
Answer:
UserManager<TUser>
is a complex class with many dependencies and virtual methods. When using AutoFixture with Moq, it's essential to inject both the mock and its object form to ensure that:
- You can configure behavior using
.Setup()
on theMock<UserManager<TUser>>
. - The SUT (System Under Test) receives the correct
UserManager<TUser>
instance via dependency injection.
Here's how to do it correctly in a custom [AutoData]
attribute:
var fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
var store = new Mock<IUserStore<ApplicationUser>>();
var userManager = new Mock<UserManager<ApplicationUser>>(
store.Object, null, null, null, null, null, null, null, null);
// Inject both the mock and the object
fixture.Inject(userManager); // For [Frozen] Mock<UserManager<TUser>>
fixture.Inject(userManager.Object); // For the actual dependency used by the SUT
Without injecting both, AutoFixture may resolve a different instance for the mock and for the injected
UserManager
, causing.Setup()
to appear ineffective and your SUT to receive an unconfigured or default instance.
Certainly. Here's the English version of the two key issues from today's session:
5.How to Inject RoleManager<T>
Properly in Unit Tests
Problem:
RoleManager<T>
(and similarly UserManager<T>
) is a complex class with multiple dependencies.
You need to manually mock and inject all required dependencies:
var roleStoreMock = new Mock<IRoleStore<IdentityRole>>();
var roleValidators = new List<IRoleValidator<IdentityRole>>();
var roleManager = new RoleManager<IdentityRole>(
roleStoreMock.Object,
roleValidators,
new UpperInvariantLookupNormalizer(),
new IdentityErrorDescriber(),
new Mock<ILogger<RoleManager<IdentityRole>>>().Object
);
Then inject it into your test fixture using:
fixture.Inject(roleManager); // For [Frozen] Mock<RoleManager<TRole>>
fixture.Inject(roleManager.Object); // For the actual dependency used by the SUT
This ensures that:
- The
RoleManager
is available for constructor injection - You can directly access and verify it in your test code
6.Why IUnitOfWork.WithTransactionAsync(Func<Task<T>>)
Didn’t Trigger Inner Logic
Problem:
You expected an exception to be thrown inside this setup:
userServiceMock
.Setup(x => x.CreateUserOrThrowInnerAsync(...))
.ThrowsAsync(new UserCreateException(...));
…but during test execution, no exception was thrown.
Root Cause:
WithTransactionAsync
takes a Func<Task<T>>
delegate. When using Moq, the delegate you pass is not automatically executed unless you explicitly invoke it in your mock setup.
You might have written:
unitOfWorkMock.Setup(x => x.WithTransactionAsync(It.IsAny<Func<Task<T>>>(), ...));
…but if you don’t invoke the Func
, the code inside (e.g. CreateUserOrThrowInnerAsync
) never runs, so the exception isn’t thrown.
Correct Fix:
Manually execute the passed delegate in your setup:
unitOfWorkMock
.Setup(x => x.WithTransactionAsync(It.IsAny<Func<Task<T>>>(), It.IsAny<CancellationToken>()))
.Returns((Func<Task<T>> func, CancellationToken _) => func());
Or encapsulate it in a reusable extension method:
public static class UnitOfWorkMockExtensions
{
public static void SetupWithTransaction<T>(this Mock<IUnitOfWork> mock)
{
mock.Setup(u => u.WithTransactionAsync(It.IsAny<Func<Task<T>>>(), It.IsAny<CancellationToken>()))
.Returns((Func<Task<T>> func, CancellationToken _) => func());
}
}
Result:
- Your service logic (inside the delegate) is actually executed
- Any exceptions thrown inside are observed by your unit test
- Your assertions are valid and test results become reliable
Top comments (0)