← Барлық мақалаларға оралу

Legacy project-ке unit tests қалай қосуға болады

Жарияланды

Сізге years бойы running болған, бірақ tests almost жоқ codebase келді. Unit tests қосу chore сияқты көрінуі мүмкін, especially project testing үшін built болмаған болса. Бірақ few simple tests кейін debugging hours сақтап қалуы мүмкін.

Бұл post ішінде неге effort worth екенін түсіндіріп, entire app rewrite талап етпейтін lightweight approach көрсетемін.

Old Code үшін неге tests жазу керек?

Қазір бастауға бірнеше good reasons:

  • Key logic қорғау. Method ішінде complex price немесе interest-rate calculation buried болуы мүмкін. Test future tweaks оны бұзбайтынына көз жеткізеді.

  • Debugging жылдамдату. Database, SMTP server немесе басқа services spin up етудің орнына, керек piece бойынша quick test run ете аласыз.

  • Small steps арқылы refactor. Full rewrite risky. Small chunks-ты testable units ішіне extracting жасау арқылы immediate benefits аласыз және bigger changes үшін confidence жинайсыз.

Humble Object Approach

Testing бастау үшін perfect architecture қажет емес. Humble Object pattern арқылы сіз:

  1. Business logic-ті external dependencies жоқ standalone class ішіне pull out етесіз.

  2. Database access, email calls, logging және басқа “plumbing” original method ішінде қалдырасыз.

  3. New simple class үшін tests жазасыз. Code қалған бөлігі untouched қалады.

Simple Example

1. Original Controller

Мына controller calculations-ты email және database calls-пен араластырады. Мұны directly test ету EF Core, DateTime.Now, SMTP және logger бәрін бірден mock жасау деген сөз.

[ApiController]
[Route("[controller]")]
public class LegacyOrderController : ControllerBase
{
    private readonly SmtpClient _smtp = new("mail.company.internal");
    private readonly ILogger _log;
    private readonly DbContext _db;

    public LegacyOrderController(DbContext db, ILogger log)
    {
        _db = db;
        _log = log;
    }

    [HttpPost("create")]
    public IActionResult Create(OrderDto dto)
    {
        var customer = _db.Customers.Find(dto.CustomerId);
        var now = DateTime.Now;
        decimal subtotal = dto.Lines.Sum(l => l.Price * l.Qty);

        // Black Friday discount
        if (now.Month == 11 && now.Day >= 25 && now.Day <= 30)
            subtotal *= 0.8m;

        decimal vat = subtotal * 0.12m;
        decimal total = subtotal + vat;

        customer.Balance -= total;
        _db.SaveChanges();

        _smtp.Send(
            "sales@corp",
            customer.Email,
            "Thanks for your order",
            $"Total = {total:C}");

        _log.Write($"{now:u} Order {dto.Id} for {customer.Id} = {total}");

        return Ok(new { id = dto.Id, total });
    }
}

2. Logic шығару

Алдымен current time алу үшін small interface анықтаймыз:

public interface IClock
{
    DateTime UtcNow { get; }
}

public class SystemClock : IClock
{
    public DateTime UtcNow => DateTime.UtcNow;
}

Tip: Егер .NET 8 немесе later қолдансаңыз, custom IClock орнына built-in TimeProvider қолдануға болады.

Кейін price calculation-ды own class ішіне move етеміз:

public class OrderPricer
{
    private readonly IClock _clock;
    private const decimal VatRate = 0.12m;

    public OrderPricer(IClock clock) => _clock = clock;

    public decimal CalculateTotal(IEnumerable<OrderLineDto> lines)
    {
        decimal subtotal = lines.Sum(l => l.Price * l.Qty);
        var now = _clock.UtcNow;

        // Apply Black Friday discount
        if (now.Month == 11 && now.Day >= 25 && now.Day <= 30)
            subtotal *= 0.8m;

        decimal vat = subtotal * VatRate;
        return subtotal + vat;
    }
}

3. Controller жаңарту

Енді controller тек wiring жасап, side effects handles етеді:

[ApiController]
[Route("[controller]")]
public class LegacyOrderController : ControllerBase
{
    private readonly DbContext _db;
    private readonly IClock _clock;
    private readonly SmtpClient _smtp = new("mail.company.internal");
    private readonly ILogger _log;

    public LegacyOrderController(
        DbContext db,
        ILogger log,
        IClock? clock = null)
    {
        _db = db;
        _clock = clock ?? new SystemClock();
        _log = log;
    }

    [HttpPost("create")]
    public IActionResult Create(OrderDto dto)
    {
        var customer = _db.Customers.Find(dto.CustomerId);
        var pricer = new OrderPricer(_clock);
        decimal total = pricer.CalculateTotal(dto.Lines);

        customer.Balance -= total;
        _db.SaveChanges();

        _smtp.Send(
            "sales@corp",
            customer.Email,
            "Thanks for your order",
            $"Total = {total:C}");

        _log.Write(
            $"{_clock.UtcNow:u} Order {dto.Id} for {customer.Id} = {total}");

        return Ok(new { id = dto.Id, total });
    }
}

4. Simple Tests жазу

Logic extracted болғанда tests straightforward болады:

public class FixedClock : IClock
{
    public DateTime UtcNow { get; init; }
}

public class OrderPricerTests
{
    [Fact]
    public void CalculatesTotalWithVat_NoDiscount()
    {
        var clock = new FixedClock
        {
            UtcNow = new DateTime(2025, 7, 19)
        };
        var pricer = new OrderPricer(clock);

        var total = pricer.CalculateTotal(new[]
        {
            new OrderLineDto { Price = 100, Qty = 1 }
        });

        Assert.Equal(112m, total);
    }

    [Fact]
    public void AppliesBlackFridayDiscount()
    {
        var clock = new FixedClock
        {
            UtcNow = new DateTime(2025, 11, 27)
        };
        var pricer = new OrderPricer(clock);

        var total = pricer.CalculateTotal(new[]
        {
            new OrderLineDto { Price = 100, Qty = 1 }
        });

        Assert.Equal(100m * 0.8m * 1.12m, total);
    }
}

Wrap-Up

Legacy code тестілеуді massive rewrite жасамай-ақ бүгін бастауға болады. Core logic-ті small, easily tested classes ішіне ғана шығару арқылы сіз:

  1. Bigger changes алдында confidence аласыз.

  2. Debugging кезінде feedback loop жылдамдатасыз.

  3. Code қалған бөлігін unchanged сақтайсыз.

Бұл pattern-ді келесі bug fix немесе feature кезінде байқап көріңіз. Бірнеше tests-тен қаншалықты тез value алатыныңыз таңғалдыратын болуы мүмкін.