Регламент по написанию тестов

Регламент по написанию тестов

Регламент составлен на основе лучших практик собранных здесь: https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices. Не смотря на то что рекомендации описаны для unit tests, там есть глубокие мысли, которые относятся ко всем типам тестов.

Предложения по процессу коддинга:

1. Новый Класс/Объекты с логикой должны быть обязательно покрыты unit тестами.

2. Если ты пофиксил баг, напиши тест который покроет этот фикс.

3. Если ты закодил фичу, должен написать мин количество интеграционных тестов на фичу.

Требования к тестам

1. Тест должен быть как можно проще. Его цель - проверить что-то одно.

2. Тест надо именовать по шаблону: КакойМетодТестируем_Сценарий_что ожидаем.

3. Тестовый метод должен соответствовать паттерну AAA (Arrange Act Assert).

4. Unit тест должен тестировать программный модуль, изолируя его от всех зависимостей.

5. Тесты по возможности не должны содержать циклов и условных конструкций, нужно использовать механизмы фреймворков для тестирования, типа [InlineData(0)].

6. В тесте не следует вызывать асинхронные методы тестируемого модуля, синхронно используя .Result/GetAwaiter().GetResult(), там где можно вызывать асинхронно (любой тестовый метод может быть асинхронным)

7. В тесте обязательно должна быть секция Assert. А вызова вспомогательного метода, в котором есть Assert, не должно быть.

8. Переиспользовать код в тестах нужно, но без логики, максимально простые операции (например, инициализация объекта).

9. Стоит избегать хардкода значений. Значения надо передавать через приватные поля тестового класса.

Тест должен быть как можно проще и должен проверять что-то одно

Плохо:

 
public async Task     
ChangePhoneNumberAsync_ExistsPhoneNumberAndCodeAreGiven_ShouldReturnBadRequest()
{
var userProfile = await CreateUserWithAvailableUserBonusPoints( 0.0M );
AuthorizeUser( userProfile );
var code =  await GenerateVerificationCodeAsync( userProfile, ExistsPhoneNumber );

	var changePhoneNumberResult = await _accountController.ChangePhoneNumberAsync( new VerifyPhoneNumberDto {
			PhoneNumber = ExistsPhoneNumber,
			Code = code});

	var changePhoneNumberUser = Context.UserProfiles
				.FirstOrDefault(u => u.Id == userProfile.Id);

changePhoneNumberUser.Should().NotBeNull();

if (changePhoneNumberUser != null) {
	changePhoneNumberUser.SecureUser.Should().NotBeNull();
	changePhoneNumberUser.SecureUser.PhoneNumber.Should().NotBe( ExistsPhoneNumber );
	changePhoneNumberUser.SecureUser.UserName.Should().NotBe( ExistsPhoneNumber );
	changePhoneNumberUser.SecureUser.NormalizedUserName.Should().NotBe( ExistsPhoneNumber );
}

changePhoneNumberResult.Should().BeOfType<BadRequestObjectResult>();

var objectResult = changePhoneNumberResult as BadRequestObjectResult;

var changeUserPhoneNumberError = objectResult?.Value as string;
changeUserPhoneNumberError.Should().Be(ErrorOnTryingChangePhoneNumberToExistsPhoneNumber);
}

Лучше:

 
[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
	var stringCalculator = new StringCalculator();

	var actual = stringCalculator.Add("0");

	Assert.Equal(0, actual);
}

Тест должен именоваться по шаблону:
КакойМетодТестируем_Сценарий_что ожидаем

 
[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
	var stringCalculator = new StringCalculator();

	var actual = stringCalculator.Add("0");

	Assert.Equal(0, actual);
}

Плохо:

 
[Fact]
public void Test_Single()
{
	var stringCalculator = new StringCalculator();

	var actual = stringCalculator.Add("0");

	Assert.Equal(0, actual);
}

Лучше:

 
[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
	var stringCalculator = new StringCalculator();

	var actual = stringCalculator.Add("0");

	Assert.Equal(0, actual);
}

Тестовый метод должен соответствовать паттерну AAA (Arrange Act Assert).

Плохо:

 
[Fact]
public void Add_EmptyString_ReturnsZero()
{
	// Arrange
	var stringCalculator = new StringCalculator();

	// Assert
	Assert.Equal(0, stringCalculator.Add(""));
}

Лучше:

 
[Fact]
public void Add_EmptyString_ReturnsZero()
{
	// Arrange
	var stringCalculator = new StringCalculator();

	// Act
	var actual = stringCalculator.Add("");

	// Assert
	Assert.Equal(0, actual);
}

Unit тест должен тестировать программный модуль, изолируя его от всех зависимостей

Плохо:

 
[Fact]
public void GetData_EmptyData_ReturnsEmptyData()
{
	// Arrange
	var context = new Context();
	var repo = new DataRepository(context);

	// Act
	var actual = repo.GetData();

	// Assert
	Assert.Equal(0, actual);
}

Лучше:

 
[Fact]
public void GetData_EmptyData_ReturnsEmptyData()
{
	// Arrange
	//эмуляция контекста
	var mockContext = new Mock<Context>().Object;
	var repo = new DataRepository(mockContext);

	// Act
	var actual = repo.GetData();

	// Assert
	Assert.Equal(0, actual);
}

Тесты по возможности не должны содержать циклов и условных конструкций, нужно использовать механизмы фреймворков для тестирования, типа [InlineData(0)]

Плохо:

 
[Fact]
public void Add_MultipleNumbers_ReturnsCorrectResults()
{
	var stringCalculator = new StringCalculator();
	var expected = 0;
	var testCases = new[]
	{
		"0,0,0",
		"0,1,2",
		"1,2,3"
	};

	foreach (var test in testCases)
	{
		Assert.Equal(expected, stringCalculator.Add(test));
		expected += 3;
	}
}

Лучше:

 
[Theory]
[InlineData("0,0,0", 0)]
[InlineData("0,1,2", 3)]
[InlineData("1,2,3", 6)]
public void Add_MultipleNumbers_ReturnsSumOfNumbers(string input, int expected)
{
	var stringCalculator = new StringCalculator();

	var actual = stringCalculator.Add(input);

	Assert.Equal(expected, actual);
}

В тесте не вызывать асинхронные методы тестируемого модуля синхронно, используя .Result/GetAwaiter().GetResult() там, где можно вызывать асинхронно (любой тестовый метод может быть асинхронным)

Плохо:

 
public void GetData_EmptyData_ReturnsEmptyData()
{
	// Arrange
	//эмуляция контекста
	var mockContext = new Mock<Context>().Object;
	var repo = new DataRepository(mockContext );

	// Act
	var actual = repo.GetDataAsync().Result;

	// Assert
	Assert.Equal(0, actual);
}

public void GetData_EmptyData_ReturnsEmptyData()
{
	// Arrange
	//эмуляция контекста
	var mockContext = new Mock<Context>().Object;
	var repo = new DataRepository(mockContext );

	// Act
	var actual = repo.GetDataAsync().GetAweter().GetResult();

	// Assert
	Assert.Equal(0, actual);
}

Лучше:

 
public async Task GetData_EmptyData_ReturnsEmptyData()
{
	// Arrange
	//эмуляция контекста
	var mockContext = new Mock<Context>().Object;
	var repo = new DataRepository(mockContext );

	// Act
	var actual = await repo.GetDataAsync();

	// Assert
	Assert.Equal(0, actual);
}

В тесте обязательно должна быть секция Assert. А вызова вспомогательного метода, в котором есть Assert, не должно быть.

Плохо:

 
[Fact]
public void GetData_EmptyData_ReturnsEmptyData()
{
	// Arrange
	//эмуляция контекста
	var mockContext = new Mock<Context>().Object;
	var repo = new DataRepository(mockContext );

	// Act
	var actual = repo.GetData();

	// Плохо
	AssertSection(actual);
}

private void AssertSection(actual)
{
Assert.Equal(0, actual);
Assert.Equal(0, actual);
Assert.Equal(0, actual);
..............
}

Лучше:

 
[Fact]
public void GetData_EmptyData_ReturnsEmptyData()
{
	// Arrange
	//эмуляция контекста
	var mockContext = new Mock<Context>().Object;
	var repo = new DataRepository(mockContext );

	// Act
	var actual = repo.GetData();

	// Assert
	Assert.Equal(0, actual);
	Assert.Equal(0, actual);
	...........
}

Переиспользовать код в тестах нужно, но без логики, максимально простые операции.

Плохо:

 
public void ValidateOrders_NotOrders_ReturnsItems()
{
	var result = BadReused();

	Assert.Equal(true, actual);
}

private Purchase BadReused()
{
	var stubOrder = new FakeOrder();
	var purchase = new Purchase(stubOrder);
	return purchase.ValidateOrders();
}

Лучше:

 
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
	var stringCalculator = CreateDefaultStringCalculator();

	var actual = stringCalculator.Add("0,1");

	Assert.Equal(1, actual);
}

private StringCalculator CreateDefaultStringCalculator()
{
	return new StringCalculator();
}

Стоит избегать хардкода значений. Значения передавать через приватные поля тестового класса

Плохо:

 
[Fact]
public void Add_BigNumber_ThrowsException()
{
	var stringCalculator = new StringCalculator();

	Action actual = () => stringCalculator.Add("1001");

	Assert.Throws<OverflowException>(actual);
}

Лучше:

 
[Fact]
void Add_MaximumSumResult_ThrowsOverflowException()
{
var stringCalculator = new StringCalculator();
const string MAXIMUM_RESULT = "1001";

Action actual = () => stringCalculator.Add(MAXIMUM_RESULT);

Assert.Throws<OverflowException>(actual);
}
Комментарии для сайта Cackle

Сервисы 1С для работы с маркетплейсами