Чому важливо писати тестований код: уроки з шаблону Factory Method

Вступ

Написання тестованого коду — це не просто гарна практика, а й справжня рятівна соломинка під час розробки та підтримки.

Нещодавно я працював над проектом, де класи були сильно пов’язані між собою, що робило юніт-тестування майже неможливим. Це фруструюче досвід змусив мене згадати, наскільки важлива чиста архітектура та проектування тестованого коду.

У цій статті я розповім вам про:

  • Чому сильно зв’язаний код важко тестувати?
  • Як шаблони проектування, такі як 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

Leave a Reply

Your email address will not be published. Required fields are marked *