Question

I am building a small web API using .NET Core. I want to practice TDD and this is my first attempt at TDD. These are a few use cases of the API:

  • Users in administrator role can create/edit/delete common(shared) lesson records.
  • Users in teacher role can create lesson record.
  • Users in teacher role can edit or delete lesson records that are created by them.
  • Lessons must have a name (cannot be empty).

So I now have something like this

using FluentValidation;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;

[Route("/lessons")]
[Authorize(Roles = "teacher, admin")]
public class LessonsController : ApiControllerBase
{
    private readonly IMediator _mediator;

    public LessonsController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost]
    [Authorize(Roles = "admin")]
    [Route("create-common-lesson")]
    public async Task<IActionResult> CreateCommonLesson([FromBody]CreateLessonCommand command)
    {
        command.UserId = null;
        return await ExecuteRequest(_mediator, command);
    }

    [HttpPost]
    [Route("create-lesson")]
    public async Task<IActionResult> CreateLesson([FromBody]CreateLessonCommand command)
    {
        command.UserId = User.FindFirst("sub")?.Value;
        return await ExecuteRequest(_mediator, command);
    }

    [HttpGet]
    [Route("common-lessons")]
    public async Task<IActionResult> GetCommonLessons()
    {
        var query = new LessonQuery();

        return Ok(await _mediator.Send(query));
    }
}

and here are some of my tests ;

using Common;
using FluentAssertions;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

using Xunit;

[Collection("Api Tests")]
public class LessonApiTests : IntegrationTestBase, IClassFixture<WebApiTestFixture>
{
    public LessonApiTests(WebApiTestFixture fixture)
        : base(fixture)
    {
    }

    [Fact]
    public async Task LessonApi_Get_Should_ReturnUnauthorized_When_TokenNotProvided()
    {
        Client.DefaultRequestHeaders.Authorization = null;
        var response = await Client.GetAsync("/lessons/common-lessons");
        response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
    }

    [Fact]
    public async Task LessonApi_GetCommonLessons_ReturnsCommonLesson_WhenTeacherRequests()
    {
        var response = await TeacherGetAsync("/lessons/common-lessons");
        response.StatusCode.Should().Be(HttpStatusCode.OK);
        var lessons = await response.Content.ReadAsAsync<IEnumerable<Lesson>>();

        lessons.Should().NotBeNullOrEmpty();
    }

    [Fact]
    public async Task LessonApi_CreateCommonLesson_ReturnsForbidden_When_UserIsATeacher()
    {
        var model = new CreateLessonCommand()
        {
            Name = "Some-Random-Name"
        };
        var content = new StringContent(
            JsonConvert.SerializeObject(model),
            Encoding.UTF8,
            "application/json");

        var response = await TeacherPostAsync("/lessons/create-common-lesson", content);

        response.StatusCode.Should().Be(HttpStatusCode.Forbidden);
    }

    [Fact]
    public async Task LessonApi_CreateCommonLesson_Creates_When_UserIsAnAdminAndValidModelPosted()
    {
        var model = new CreateLessonCommand()
        {
            Name = "Some-Random-Name"
        };
        var content = new StringContent(
            JsonConvert.SerializeObject(model),
            Encoding.UTF8,
            "application/json");

        var response = await AdminPostAsync("/lessons/create-common-lesson", content);

        response.StatusCode.Should().Be(HttpStatusCode.OK);
        response = await TeacherGetAsync("/lessons/common-lessons");
        var lessons = await response.Content.ReadAsAsync<IEnumerable<Lesson>>();
        lessons.Should().Contain(l => l.Name == "Some-Random-Name");
    }

    [Fact]
    public async Task LessonApi_CreateCommonLesson_ReturnsBadRequest_WhenEmptyNamePosted()
    {
        var model = new CreateLessonCommand();
        var content = new StringContent(
            JsonConvert.SerializeObject(model),
            Encoding.UTF8,
            "application/json");

        var response = await AdminPostAsync("/lessons/create-common-lesson", content);

        response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
        response.Content.ReadAsStringAsync().Result.Should().Contain(Constants.ErrorCodes.Lesson.NameCannotBeEmpty);
    }

    [Fact]
    public async Task LessonApi_CreateLesson_Creates_WhenValidModelProvided()
    {
    }
}

I am trying to write tests for every use case scenario. However, I feel like I am off track here.

My questions are:

Is writing tests for every use case scenario good practice or bad practice?

Should the name of the integration test explain the use case or should it explain how the API will behave? For example, should it be something like this:

LessonApi_CreateCommonLesson_Returns_BadRequest_When_EmptyNamePosted

or like this:

Admin_CannotCreateCommonLesson_When_NameIsEmpty
Was it helpful?

Solution

A common naming convention for tests (unit, AAT, and integration) is "Given-When-Then" (or GWT). Each portion will roughly match one of the three A's - Arrange is "Given", Act is the "When", and Assert is the "Then". It is common to preface the test name for searchability - maybe with the specific object being tested (especially in unit tests), but this may also be a general area of the software.

Applying this to your example, this would yield something like LessonApi_GivenCreatingCommonLesson_WhenEmptyNamePosted_ThenBadRequestIsReturned.

Bear in mind that test suites are often used as a digging-in-point for people that are new to your codebase, so searchability is really important; to that end, using a name that includes words that you can easily see someone searching for can be helpful to future readers of your code (including "you, in five years").

Licensed under: CC-BY-SA with attribution
scroll top