Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Основные правила написания тестов для C# Backend
Написание эффективных и поддерживаемых тестов — критически важный навык для backend-разработчика. Вот ключевые правила и принципы, которых я придерживаюсь:
1. Принципы FIRST
Тесты должны соответствовать акрониму FIRST:
-
Fast (Быстрые): Тесты должны выполняться быстро, чтобы их можно было запускать часто. Медленные тесты (особенно интеграционные) обычно выносятся в отдельную фазу CI/CD.
-
Independent (Независимые): Каждый тест должен быть изолирован и не зависеть от состояния, созданного другими тестами. Это гарантирует стабильность и предсказуемость.
-
Repeatable (Повторяемые): Тест должен давать одинаковый результат при каждом запуске в любой среде (локально, на CI-сервере).
-
Self-Validating (Самопроверяющиеся): Тест должен однозначно определять — пройден он или упал, без необходимости ручной проверки.
-
Timely (Своевременные): Тесты пишутся одновременно с кодом (TDD) или сразу после него, а не в конце разработки.
2. Структура AAA (Arrange-Act-Assert)
Каждый тест должен четко разделяться на три фазы:
[Fact]
public void CalculateTotal_WithMultipleItems_ReturnsCorrectSum()
{
// Arrange: Подготовка данных и зависимостей
var cart = new ShoppingCart();
cart.AddItem(new CartItem { Price = 100, Quantity = 2 });
cart.AddItem(new CartItem { Price = 50, Quantity = 1 });
var calculator = new PriceCalculator();
// Act: Выполнение тестируемого действия
decimal total = calculator.CalculateTotal(cart);
// Assert: Проверка результата
Assert.Equal(250, total);
}
3. Правила именования тестов
Имя теста должно описывать что тестируется, при каких условиях и ожидаемый результат. Популярные конвенции:
MethodName_StateUnderTest_ExpectedBehaviorShould_ExpectedBehavior_When_StateUnderTest
// Пример хорошего именования
[Fact]
public void ProcessOrder_WithInvalidStatus_ThrowsOrderException()
{
// ...
}
[Fact]
public void Should_ReturnNull_When_UserNotFound()
{
// ...
}
4. Изоляция и Mocking
- Используйте моки для внешних зависимостей (базы данных, API, файловой системы)
- Предпочитайте конструктор внедрения зависимостей для легкого тестирования
- Избегайте статических классов и синглтонов в бизнес-логике
public class OrderServiceTests
{
[Fact]
public void PlaceOrder_ValidRequest_CallsRepositoryAndPublisher()
{
// Arrange
var mockRepo = new Mock<IOrderRepository>();
var mockPublisher = new Mock<IMessagePublisher>();
var service = new OrderService(mockRepo.Object, mockPublisher.Object);
// Act
service.PlaceOrder(new OrderRequest());
// Assert
mockRepo.Verify(r => r.Save(It.IsAny<Order>()), Times.Once);
mockPublisher.Verify(p => p.Publish(It.IsAny<OrderPlacedEvent>()), Times.Once);
}
}
5. Покрытие и качество
- Тестируйте поведение, а не реализацию: Тесты должны проверять, что код делает то, что нужно, а не как он это делает.
- Избегайте тестирования тривиального кода (геттеры/сеттеры, простые маппинги).
- Стремитесь к высокому покрытию критических путей, но не гонитесь за 100% покрытием любой ценой.
- Используйте метрики осмысленно: 70% хорошо написанных тестов лучше, чем 95% хрупких.
6. Организация тестов
- Разделяйте юнит-тесты, интеграционные и e2e тесты
- Используйте разные проекты или папки для разных типов тестов
- Группируйте тесты по функциональности, а не по техническому признаку
Tests/
├── Unit/
│ ├── Services/
│ ├── Domain/
│ └── Application/
├── Integration/
│ ├── DatabaseTests/
│ └── ApiTests/
└── EndToEnd/
7. Работа с данными
- Используйте тестовые данные непосредственно в тестах (встроенные или из фабрик)
- Для интеграционных тестов применяйте подходы:
- Testcontainers для изолированных БД
- In-memory базы данных (с осторожностью)
- Транзакции с откатом
public class UserRepositoryTests : IClassFixture<DatabaseFixture>
{
[Fact]
public async Task GetUser_ExistingId_ReturnsUser()
{
using var context = new TestDbContext();
// Arrange: Явное добавление тестовых данных
var testUser = new User { Id = 1, Name = "Test" };
context.Users.Add(testUser);
await context.SaveChangesAsync();
// Act & Assert
var user = await context.Users.FindAsync(1);
Assert.NotNull(user);
Assert.Equal("Test", user.Name);
}
}
8. Антипаттерны и что избегать
❌ Хрупкие тесты, зависящие от времени, случайных данных или порядка выполнения
❌ Тесты без ассертов или с пропущенными исключениями
❌ Избыточные проверки одного и того же в разных тестах
❌ Тестирование приватных методов через рефлексию
❌ Игнорирование падающих тестов ("пока закомментирую")
9. Современные практики для C#
- Используйте FluentAssertions для читаемых проверок:
result.Should().NotBeNull()
.And.Subject.As<User>().Name.Should().Be("John");
- Применяйте параметризованные тесты для проверки граничных значений:
[Theory]
[InlineData(0, 0)]
[InlineData(1, 1)]
[InlineData(5, 120)]
[InlineData(10, 3628800)]
public void Factorial_ValidInput_ReturnsCorrectResult(int n, long expected)
{
var result = MathHelper.Factorial(n);
Assert.Equal(expected, result);
}
- Настраивайте тестовое окружение через конструкторы и фикстуры в xUnit
10. Интеграция в процесс разработки
- Тесты — часть Definition of Done
- Все тесты должны проходить перед созданием PR
- Регрессионные тесты добавляются для каждого исправленного бага
- Рефакторинг тестов так же важен, как рефакторинг основного кода
Качество тестового кода напрямую влияет на скорость разработки и стабильность системы. Хорошо написанные тесты служат живой документацией, позволяют безопасно рефакторить и дают уверенность при развертывании изменений. В контексте C# backend особенно важно уделять внимание тестированию асинхронных операций, работы с БД, интеграции с внешними сервисами и обработке исключений.