Mastering Code Testing: Harnessing SUT or Fixtures

Dive into the world of code testing with this comprehensive guide

Nash
4 min readApr 8, 2024
Mastering code testing (DALL-E)

In the bustling arena of software development, ensuring that applications are robust and free from defects is paramount. However, the journey of writing tests is often seen as a time-consuming task that undergoes frequent revisions alongside production code changes. This issue signals a deeper problem: poor testing design.

While it’s inevitable that tests will evolve as production evolves, the impact should be minimal. If a single change in production code requires multiple tests to be updated, it might be time to reconsider how these tests are written.

The Art of Test Writing

Your tests should be as clean and well-structured as your production code, perhaps even more so. Tests are not just code; they encapsulate the functionality and intentions of your software. They act as the blueprint for what your application can do.

If faced with the choice between deleting all my production code or all my test code, I’d opt to erase the production code.

Why? Because with a comprehensive test suite in place, rebuilding the software becomes a feasible, systematic task, signaling the end of development with certainty.

The Power of SUT or Fixture

The concept of the System Under Test (SUT) or a fixture (beyond the xUnit definition) serves as an excellent strategy to decouple test descriptions from their implementation.

These entities should promote clarity, brevity, and comprehensibility in your tests. A well-crafted SUT or fixture keeps your tests focused and straightforward, enabling anyone to grasp the tested functionality at a glance.


public class CreateAnAbsenceCommandHandlerTests
{
private readonly CreateAnAbsenceFixture fixture;

public CreateAnAbsenceCommandHandlerTests()
{
this.fixture = new CreateAnAbsenceFixture();
}

[Fact]
public async Task AnAbsenceCanBeCreated()
{
await this.fixture
.CreateAnAbsence();

this.fixture.Should().HaveCreatedAbsence();
}

[Fact]
public async Task canCreateAbsenceWithSocialSecurityNature()
{
await this.fixture
.WithSocialSecurityNature()
.CreateAnAbsence();

this.fixture.Should().HaveCreatedAbsenceWithSocialSecurityNature();
}

[Theory]
[InlineData("02-01-2022 00:00", "01-01-2022 23:00")]
[InlineData("02-01-2022 00:00", "01-01-2022 12:00")]
[InlineData("02-01-2022 13:00", "02-01-2022 12:00")]
public async Task ShouldNotCreateAnAbsenceWithDateEndLowerOrEqualsThanDateStart(String start, String end)
{
DateTime startDate = start.ToDateTimeUtc();
DateTime endDate = end.ToDateTimeUtc();

await this.fixture
.WithDates(startDate, endDate)
.CreateAnAbsence();

this.fixture.Should().HaveThrowValidationException();
}

[Fact]
public async Task EmployeeApplicantCantCreateAnAbsenceForAnotherEmployeeIfNoPermission()
{
await this.fixture
.ForEmployeeId(1)
.AsApplicantId(2)
.WithoutPermissionToCreateForOther()
.CreateAnAbsence();

this.fixture.Should().HaveThrowSecurityException();
}
}

In this snippet, CreateAnAbsence is our SUT. The tests succinctly articulate the business rules around creating absences, leveraging the fixture to manage state and interactions.

Designing an Effective Fixture

A well-designed fixture in C# encapsulates the initial valid state of the system, reducing redundant setup code. It should offer methods to modify this state in a clear and concise manner, thus ensuring that the test remains focused on the behavior rather than the implementation details.


public class CreateAnAbsenceFixture : CommandFixture
{
private readonly AbsencePermissionRepositoryMock absencePermissionRepository;
public readonly CalendarRepositoryMock calendarRepository;
public readonly UserContextFactoryMock userContextFactory;

private readonly AbsenceAddCommand absenceRequest = new()
{
BeginDate = "01-01-2022 00:00".ToDateTimeUtc(),
EndDate = "25-01-2022 23:00".ToDateTimeUtc(),
EmployeeId = 1,
NatureId = NatureId.PaidLeave
StatusId = StatusId.New
};


public CreateAnAbsenceFixture()
{
this.absencePermissionRepository = new AbsencePermissionRepositoryMock();
this.calendarRepository = new CalendarRepositoryMock();
this.userContextFactory = new UserContextFactoryMock();
}


public CreateAnAbsenceFixture WithOneSegmentOfSocialSecurityNature()
{
this.absenceRequest.NatureId = NatureId.SocialSecurity;
return this;
}

public CreateAnAbsenceFixture ForEmployeeId(Int64 employeeId)
{
this.absenceRequest.EmployeeId = employeeId;
return this;
}


public CreateAnAbsenceFixture WithDates(DateTime startAt, DateTime endAt)
{
this.absenceRequest.BeginDate = startAt;
this.absenceRequest.EndDate = endAt;
return this;
}

public CreateAnAbsenceFixture AsApplicantId(Int32 applicantId)
{
this.userContextFactory.User = this.userContextFactory.User with { Id = new EmployeeId(applicantId) };
return this;
}

public CreateAnAbsenceFixture WithoutPermissionToCreateForOther()
{
this.absencePermissionRepository.UserCanEditAbsence = AuthorizeResult.NoPermissionsFor(this.userContextFactory.User.Id, AbsencePermission.Write.ToString());
this.userContextFactory.SetUserRoleType(UserRoleType.Employee);
return this;
}

public async Task CreateAnAbsence()
{
this.absenceRequest.Segments = this.segments;
await CallCommand(this.absenceRequest, ConfigureServices);
}

private void ConfigureServices(IServiceCollection services)
{
services.AddPaidLeaveContext();
services.AddScoped<IUserContextFactory>(_ => this.userContextFactory);
services.AddScoped<IAbsencePermissionRepository>(_ => this.absencePermissionRepository);
services.AddScoped<ICalendarAggregateRepository>(_ => this.calendarRepository);
}
}

This setup not only makes your tests more robust against changes in the implementation but also enhances their readability and expressiveness.

Strategic Advantages

  • Robustness to Changes: Centralizing the modification of test state in fixtures minimizes the ripple effect of changes in production code on your test suite.
  • Cleaner Tests: A focus on business logic rather than technical details makes tests more understandable and relevant.
  • Reusability and Maintainability: Object-oriented principles allow for shared setup logic across tests, reducing duplication and fostering easier maintenance.

Conclusion

Effective code testing is an intricate dance of balancing technical precision with business intent. By structuring your tests around business needs and using SUTs and fixtures, you create a resilient and adaptable testing environment. Keep your tests concise, business-focused, and isolated from unnecessary implementation details, and you’ll build a test suite that not only validates functionality but also embodies the core principles of your application’s design.

--

--