مفهوم Test Doubles - بخش دوم

در بخش اول در مورد test doubleها و SUT صحبت کردیم که با یک کامپوننت یا سرویس بیرونی (DOC) در ارتباط می باشد. و در یک مثال ساده یک نمونه Stub به صورت کانفیگ شده نوشتیم و از آن به عنوان جایگزین DOC استفاده کردیم. و همچنین اشاره شد که رابطه بین SUT و DOC یک رابطه Indirect Input می باشد.Test Doubles در یک مثال دیگر، رابطه indirect output و طریقه verification کردن آن را بررسی می کنیم.public class Person { public Person(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } public string FirstName { get; } public string LastName { get; } } public interface IPersonRepositoy { void Insert(Person person); } public class PersonService { private readonly IPersonRepositoy _personRepositoy; public PersonService(IPersonRepositoy personRepositoy) { _personRepositoy = personRepositoy; } public void ResigterPerson(string firstName,string lastName) { var person = new Person(firstName , lastName); _personRepositoy.Insert(person); } }در اینجا رابطه PersonService در نقش SUT و Repository در نقش DOC، یک رابطه output می باشد.بنابراین در اینجا SUT state تغییر نمی کند و خروجی نیز نداریم. صرفا یک interaction داریم و می خواهیم آن را تست کنیم.[Fact] public void save_person_into_database() { var mockRepository = new Moq.Mock<IPersonRepositoy>(); var personService = new PersonService(mockRepository.Object); var expected = new ResigterPersonDto { FirstName = &quotMohsen&quot, LastName = &quotFarokhi&quot, }; personService.ResigterPerson(expected.FirstName , expected.LastName); mockRepository.Verify (c => c.Insert(Moq.It.Is<Person> (c => c.FirstName == expected.FirstName && c.LastName == expected.LastName)), Moq.Times.Once); }همانطور که ملاحظه می کنید در این تست از فریم ورک Moq استفاده کردیم و مشخص کردیم که این تابع از repository باید یکبار با مقادیر مشخص شده اجرا شده باشد.Dummy یکی از ساده ترین انواع Dummy ،test double می باشد. زمانی که ما یک dependency داریم و آن dependency نقشی در تست ندارد و صرفا می خواهیم پارامترها را ست کنیم، از Dummy استفاده می کنیم. مقدار null می تواند ساده ترین نوع Dummy باشد. اما گاهی اوقات ممکن است مقدار null باعث ایجاد خطا شود. بنابراین می توانیم در این مواقع یک پیاده سازی خالی از آن dependency ایجاد کنیم و در تست استفاده کنیم. اصلاحا به آن Null Object نیز گفته می شود. Fake یک کامپوننت را در نظر بگیرید که یک کار پر هزینه ای را انجام می دهد. برای مثال می توانیم دیتابیس را در نظر بگیریم. در Fake رفتاری مشابه را پیاده سازی می کنیم با این تفاوت که سریع و کم هزینه می باشد. برای نمونه می توانیم In-memory database را نام ببریم. در حقیقت کامپوننتی را برای جایگزینی در نظر می گیریم که SUT به آن وابسته می باشد اما سریع و کم هزینه است.Stub در Stub یک شی داده های از پیش تعریف شده را نگه می دارد و از آن ها برای پاسخ به فراخوانی ها در تست استفاده می کند. در نتیجه در Stub به یک کامپوننت وابستگی داریم که دارای رابطه Indirect Input می باشد.در حقیقت نتیجه ای که می خواهیم به ما برگردانده شود را خود ما تعریف می کنیم. مثال WageRepository که در مطالب قبل پیاده سازی شد، در قالب یک test stub می باشد. Mock زمانی که قصد اجرای کدهای پیاده سازی شده را نداریم و یا راه آسانی برای تایید اجرای آن ها وجود ندارد، از Mock استفاده می کنیم. برای مثال می توانیم سرویس ارسال ایمیل را در نظر بگیریم. ما نمی خواهیم به ازای هر بار تست ایمیل نیز ارسال شود. علاوه بر این، در تست ها تأیید اینکه ایمیل درستی ارسال شده است آسان نیست. تنها کاری که می‌توانیم انجام دهیم این است که خروجی‌های عملکردی را که در تست ما اعمال می‌شود، تأیید کنیم. مثال PersonService یک نمونه از Mock بود که Verify آن به صورت Interaction Verification انجام شد.Spyدر Test Spy بر خلاف mock object، از روی رفتاری که اتفاق افتاده است تست را fail نمی کنیم. بنابراین تست ها این فرصت را دارند که اطلاعات بیشتری را در assertion استفاده کنند. در spy می توانیم assertion خوانا تری نسبت به mock داشته باشیم و همچنین پیام های مناسب تری را در assertion می توانیم قرار دهیم.در آخرین مثال مفهوم Test Spy را مورد بررسی قرار می دهیم. در اولین حالت یک کلاس test spy را به عنوان کلاس جایگزین تعریف می کنیم که اطلاعات ثبت شده را به ما بر می گرداند.public class PersonRepositorySpy : IPersonRepositoy { private Person _person; private int _callNumbers; public void Insert(Person person) { _person = person; _callNumbers++; } public Person GetSavedPerson() { return _person; } public int GetNumberOfSavedPerson() { return _callNumbers; } }و در تست از آن استفاده می کنیم.[Fact] public void saves_person_on_registration() { var repository = new PersonRepositorySpy(); var service = new PersonService(repository); var expected = new ResigterPersonDto { FirstName = &quotMohsen&quot, LastName = &quotFarokhi&quot, }; service.ResigterPerson(request: expected); var actual = repository.GetSavedPerson(); repository.GetNumberOfSavedPerson().Should().Be(1); actual.FirstName.Should().Be(expected.FirstName); actual.LastName.Should().Be(expected.LastName); }و در حالت بعدی که اصطلاحا به آن Self Shunt گفته می شود، در خود تست اینترفیس را پیاده سازی می کنیم.public class PersonServiceTest_SelfShunt : IPersonRepositoy { private Person _savedPerson; private int _callNumbers; public void Insert(Person person) { _savedPerson = person; _callNumbers++; } [Fact] public void saves_person_on_registration() { var service = new PersonService(this); var expected = new ResigterPersonDto { FirstName = &quotMohsen&quot, LastName = &quotFarokhi&quot, }; service.ResigterPerson(request: expected); _callNumbers.Should().Be(1); _savedPerson.FirstName.Should().Be(expected.FirstName); _savedPerson.LastName.Should().Be(expected.LastName); } }پایان