Вступ
Написання тестованого коду — це не просто гарна практика, а й справжня рятівна соломинка під час розробки та підтримки.
Нещодавно я працював над проектом, де класи були сильно пов’язані між собою, що робило юніт-тестування майже неможливим. Це фруструюче досвід змусив мене згадати, наскільки важлива чиста архітектура та проектування тестованого коду.
У цій статті я розповім вам про:
- Чому сильно зв’язаний код важко тестувати?
- Як шаблони проектування, такі як Factory Method, можуть вирішити цю проблему?
- Як інструменти, як-от xUnit та NSubstitute, можуть спростити тестування?
Проблема: Код, який важко тестувати
Розглянемо типовий приклад: клас, який безпосередньо створює свої залежності. Такий дизайн створює сильний зв’язок і робить юніт-тестування практично неможливим.
Ось швидкий приклад, щоб проілюструвати проблему:
public class PaymentService
{
private readonly string _paymentMethod;
public PaymentService(string paymentMethod)
{
_paymentMethod = paymentMethod;
}
//клієнт
public void ProcessPayment(decimal amount)
{
switch (_paymentMethod)
{
case "PayPal":
var paypalProcessor = new PayPalPaymentProcessor();
paypalProcessor.ProcessPayment(amount);
break;
case "CreditCard":
var creditCardProcessor = new CreditCardPaymentProcessor();
creditCardProcessor.ProcessPayment(amount);
break;
default:
throw new InvalidOperationException("Unknown payment method");
}
}
}
У наведеному коді клас PaymentService
безпосередньо створює екземпляри класів PayPalPaymentProcessor
та CreditCardPaymentProcessor
, що ускладнює заміну PaymentProcessor
на мок для тестування. Класи PayPalPaymentProcessor
і CreditCardPaymentProcessor
успадковуються від інтерфейсу IPaymentProcessor
. Ось їхній код:
public interface IPaymentProcessor
{
void ProcessPayment(decimal amount);
}
public class PayPalPaymentProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing PayPal payment of {amount:C}");
}
}
public class CreditCardPaymentProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing Credit Card payment of {amount:C}");
}
}
❌Спроба протестувати код
Давайте подивимося, що станеться, коли ми спробуємо написати юніт-тест для PaymentService
з використанням NSubstitute.
public class PaymentServiceTest
{
[Fact]
public void Call_Correct_Payment()
{
// Спроба замокати Payment Processor
var mockPaymentProcessor = Substitute.For(); // Це не спрацює
var paymentService = new PaymentService("PayPal"); // Створює реальний PaymentService
// Act
paymentService.ProcessPayment(100m);
// Assert
mockPaymentProcessor.Received(1).ProcessPayment(100m);
// Помилка: неможливо замокати PaymentProcessor, створений всередині PaymentService
}
}
Як ви можете побачити, тест не проходить, оскільки клас PaymentService
безпосередньо створює PaymentProcessor
. Ми не можемо замінити його на мок для тестування.
✅Рішення: Шаблон проектування Factory Method
Шаблон проектування Factory Method відокремлює створення об’єкта від класу, що полегшує заміну залежностей під час тестування.
Ось як ми можемо рефакторити PaymentService
, щоб використовувати шаблон Factory Method:
public class PaymentService
{
private readonly PaymentProcessorFactory _paymentProcessorFactory;
public PaymentService(PaymentProcessorFactory paymentProcessorFactory)
{
_paymentProcessorFactory = paymentProcessorFactory;
}
public void ProcessPayment(decimal amount)
{
var processor = _paymentProcessorFactory.CreatePaymentProcessor();
processor.ProcessPayment(amount);
}
}
Як ви бачите, тепер PaymentService
більше не відповідає за прийняття рішень щодо методів оплати і також не залежить безпосередньо від PaymentProcessor
.
Ось код PaymentProcessFactory
:
public abstract class PaymentProcessorFactory
{
public abstract IPaymentProcessor CreatePaymentProcessor();
}
public class PayPalFactory : PaymentProcessorFactory
{
public override IPaymentProcessor CreatePaymentProcessor()
{
return new PayPalPaymentProcessor();
}
}
// Конкретний творець - CreditCardFactory
public class CreditCardFactory : PaymentProcessorFactory
{
public override IPaymentProcessor CreatePaymentProcessor()
{
return new CreditCardPaymentProcessor();
}
}
✅Написання тестів з xUnit та NSubstitute
Тепер, коли PaymentService
залежить від інтерфейсу PaymentProcessorFactory
, ми можемо легко замокати залежність і написати юніт-тест.
public class PaymentServiceTests
{
[Fact]
public void Should_CreatePaymentProcessor()
{
// Arrange
var paymentProcessFactoryMock = Substitute.For();
var paymentService = new PaymentService(paymentProcessFactoryMock);
//Act
paymentService.ProcessPayment(100.00m);
//Assert
paymentProcessFactoryMock.Received(1).CreatePaymentProcessor();
}
[Fact]
public void Should_Process_Payment_With_Valid_Amount()
{
var amount = 100.00m;
// Arrange
var paymentProcessFactoryMock = Substitute.For();
var paymentProcessorMock = Substitute.For();
var paymentService = new PaymentService(paymentProcessFactoryMock);
paymentProcessFactoryMock.CreatePaymentProcessor().Returns(paymentProcessorMock);
//Act
paymentService.ProcessPayment(amount);
//Assert
paymentProcessorMock.Received(1).ProcessPayment(amount);
}
}
Тест тепер працює без проблем, оскільки клас PaymentSerivice
більше не залежить від реалізації PaymentProcessor
.
Це показує, як шаблон проектування Factory Method покращує тестованість, дозволяючи впроваджувати залежності.
Висновок
Написання тестованого коду — це не лише про дотримання кращих практик, а й про те, щоб зробити процес розробки більш плавним, а програмне забезпечення — більш підтримуваним.
Застосовуючи шаблон проектування Factory Method, ви можете відокремити залежності і спростити тестування.
Інструменти, такі як xUnit та NSubstitute, ще більше спрощують ваш робочий процес, дозволяючи легко перевіряти поведінку вашого коду.
Чи стикалися ви з проблемами через сильно зв’язані частини коду? Як ви забезпечуєте тестованість вашого коду? Поділіться своїми думками в коментарях або звертайтеся — буду радий обговорити!🙌
Перекладено з: Why Writing Testable Code Matters: Lessons from the Factory Method Pattern