اگر در حوزه توسعه تست محور (TDD) فعالیت داشته اید، مطمئنم با اصطلاحی به نام Test Doubles نیز مواجه شده اید. همچنین اگر unit test نیز نوشته اید مطمئنم از test doubleها استفاده نیز کرده اید. برخی از برنامه نویسان در اشاره به test doubleها از اصطلاح mock برای آن استفاده می کنند که در ادامه خواهیم دید که mock جزئی از test doubleها می باشد.
در حقیقت test double، یک شی می باشد که به دلایلی به جای شی واقعی در تست ما قرار می گیرد. مانند یک بدلکار که در یک فیلم به جای بازیگر اصلی قرار می گیرد.
وقتی در مورد Unit Test صحبت می کنیم، در حقیقت در مورد یک تست کوچک و سریع صحبت می کنیم و توقع داریم نتیجه آن را نیز به سرعت دریافت کنیم.
برای استفاده از test doubleها، ساختار SUT ما باید به شکلی باشد که بتوانیم وابستگی را از کلاس خود حذف کنیم و در اینجا موضوع dependency injection مطرح می شود.
برای درک بیشتر، مساله زیر را در نظر بگیرید که از طریق آن قصد داریم مقدار کارمزد یک مبلغ را محاسبه کنیم و نتیجه را برگردانیم.
public class WageService { private readonly IWageRepository _wageRepository; public WageService(IWageRepository wageRepository) { _wageRepository = wageRepository; } public decimal Calculate(decimal amount) { var wagePercent = _wageRepository.GetCurrentWagePercent(); var wageValue = wagePercent * amount / 100; return amount - wageValue; } } public interface IWageRepository { decimal GetCurrentWagePercent(); } public class WageRepository : IWageRepository { public decimal GetCurrentWagePercent() { // read from db throw new System.NotImplementedException(); } } public class DamageServiceTest { [Fact] public void wage_is_subtracted_from_amount() { var repository = new WageRepository(); var service = new Wage.WageService(repository); var actual = service.Calculate(amount: 1000000); actual.Should().Be(expected: 995000); } }
فرض می کنیم که تابع GetCurrentWagePercent، مقدار کارمزد را از دیتابیس خوانده و برمی گردند.
اجزای این مثال شامل depended-on component (DOC) ،system under test (SUT) و Test می باشد.
از طریق SUT ،test را صدا می زنیم و SUT یک مقدار را از DOC و در نتیجه از دیتابیس دریافت می کند و بر می گرداند.
به طور کلی زمانی موضوع test double پیش می آید که در تست خود یک یا چند DOC داریم که به تست ما سربار اضافه می کنند و یا ممکن است رفتار متفاوتی را از آن ها ببینیم. از test double در جاهایی که DOC برای ما مشکل ایجاد می کند و تست ما را از حالت unit خارج می کند، استفاده می کنیم. از طریق test double می توانیم برای DOC یک جایگزین کم هزینه و قابل پیش بینی در نظر بگیریم.
در نظر بگیرید که در این مثال ما باید یک دیتابیس داشته باشیم که دارای دیتا باشد و آماده کوئری زدن باشد. موضوع بعدی پیچیدگی است که در تست ما ایجاد می شود. برای مثال اگر بخواهیم تست خود را با مقادیر دیگر انجام دهیم دچار مشکل خواهیم شد.
در ادامه، یک پیاده سازی کم هزینه از WageRepository ایجاد کنیم و از آن استفاده کنیم.
public class StubWageRepository : IWageRepository { public decimal GetCurrentWagePercent() { return 0.5M; } }
[Fact] public void wage_is_subtracted_from_amount() { var repository = new StubWageRepository(); var service = new WageService(repository); var calculatedAmount = service.Calculate(1000000); calculatedAmount.Should().Be(995000); }
در تستی که نوشتیم بدون اینکه داده ای را از دیتابیس دریافت کنیم، از طریق کلاس جایگزین مقدار 0.5 را برمی گردانیم و از آن استفاده می کنیم.
مشکلی که در اینجا ملاحظه می کنید استفاده از مقدار hard code در repository می باشد.
همچنین مقداری در تست وجود دارد که در بدنه تست دیده نمی شود و در نتیجه ی تست تاثیر گذار است. اصطلاحا به این مشکل در تست، mystery guest گفته می شود.
بنابراین نیاز داریم که StubWageRepository خود را مجهز به کانفیگ کنیم.
public class StubWageRepository : IWageRepository { private decimal _wagePercent; public void WithWagePercent(decimal wagePercent) { _wagePercent = wagePercent; } public decimal GetCurrentWagePercent() { return _wagePercent; } }
به این ترتیب می توانیم تست خود را به InlineData مجهز کنیم و سرویس خود را با مقادیر متفاوتی تست کنیم.
[InlineData(1000000, 0.5, 995000)] [InlineData(1000000, 1, 990000)] [Theory] public void wage_is_subtracted_from_amount (decimal amount, decimal wagePercent, decimal expected) { var repository = new StubWageRepository(); repository.WithWagePercent(wagePercent); var service = new WageService(repository); var actual = service.Calculate(amount); actual.Should().Be(expected); }
نوع Verification که در این مثال انجام دادیم Output Verification بود. به این صورت که مقدار خروجی را دریافت می کنیم و آن را بررسی می کنیم.
انواع test doubleها شامل Fake ،Dummy ،Spy ،Mock ،Stub می باشد.
مثالی که در این مطلب به آن پرداخته شد یک نمونه از Stub بود و در مطالب بعدی به انواع دیگر از test doubleها می پردازیم.
رابطه بین SUT و DOC به دو شکل Indirect Input و Indirect Output می باشد.
زمانی که از DOC مقداری به SUT برگردانده می شود، آن رابطه یک Indirect Input می باشد. در مثالی که زدیم، WageService یک مقدار را از DOC خود می گرفت. بنابراین یک رابطه Input می باشد.
و زمانی که به DOC یک مقدار ورودی می دهیم، آن رابطه Indirect Output می باشد. برای مثال یک Logger رو در نظر بگیرید که صرفا یک مقدار را به عنوان ورودی از ما دریافت می کند و Log می کند.
هر دو حالت Indirect Input و Indirect Output را می توانیم بصورت همزمان نیز داشته باشیم.
پایان