آماده سازی زیرساخت تهیه Integration Tests برای ServiceLayer
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: چهار دقیقه

پیشنیاز

در این مطلب قصد داریم تست ServiceLayer را به جای تست درون حافظه‌ای که با ابزارهای Mocking در قالب Unit Testing انجام میگیرد، به کمک یک دیتابیس واقعی سبک وزن در قالب Integration Testing انجام دهیم.


قدم اول

یک پروژه تست را ایجاد کنید؛ بهتر است برای نظم دهی به ساختار Solution، پروژه‌های تست را در پوشه ای به نام Tests نگهداری کنید.



قدم دوم

بسته‌های نیوگت زیر را نصب کنید:

PM> install-package NUnit
PM> install-package Shouldly
PM> install-package EntityFramework
PM> install-package FakeHttpContext


قدم سوم

نسخه دیتابیس انتخابی برای تست خودکار، LocalDB می باشد. لازم است در ابتدای اجرای تست‌ها دیتابیس مربوط به Integration Test ایجاد شده و بعد از اتمام نیز دیتابیس مورد نظر حذف شود؛ برای این منظور از کلاس TestSetup استفاده خواهیم کرد.

[SetUpFixture]
public class TestSetup
{
    [OneTimeSetUp]
    public void SetUpDatabase()
    {
        DestroyDatabase();
        CreateDatabase();
    }

    [OneTimeTearDown]
    public void TearDownDatabase()
    {
        DestroyDatabase();
    }

   //...
}

با توجه به اینکه کلاس TestSetup با [SetUpFixture] تزئین شده است، Nunit قبل از اجرای تست‌ها سراغ این کلاس آمده و متد SetUpDatebase را به دلیل تزئین شدن با [OneTimeSetUp]، قبل از اجرای تست‌ها و متد TearDownDatabase را بدلیل تزئین شدن با [OneTimeTearDown]  بعد از اجرای تمام تست‌ها، اجرا خواهد کرد.


متد CreateDatabase

private static void CreateDatabase()
{
    ExecuteSqlCommand(Master, string.Format(SqlResource.DatabaseScript, FileName));

    //Use T-Sql Scripts For Create Database
    //ExecuteSqlCommand(MyAppTest, SqlResources.V1_0_0);

    var migration =
        new MigrateDatabaseToLatestVersion<ApplicationDbContext, DataLayer.Migrations.Configuration>();
    migration.InitializeDatabase(new ApplicationDbContext());

}

private static SqlConnectionStringBuilder Master =>
    new SqlConnectionStringBuilder
    {
        DataSource = @"(LocalDB)\MSSQLLocalDB",
        InitialCatalog = "master",
        IntegratedSecurity = true
    };

private static string FileName => Path.Combine(
    Path.GetDirectoryName(
        Assembly.GetExecutingAssembly().Location),
    "MyAppTest.mdf");

برای مدیریت محل ذخیره سازی فایل‌های دیتابیس، ابتدا دستورات ایجاد «دیتابیس تست» را برروی دیتابیس master اجرا می‌کنیم و در ادامه برای ساخت جداول از مکانیزم Migration خود EF استفاده شده است.

لازم است رشته اتصال به این دیتابیس ایجاد شده را در فایل App.config پروژه تست قرار دهید:

<connectionStrings>
  <add name="DefaultConnection" providerName="System.Data.SqlClient" connectionString="Data Source=(LocalDB)\MSSQLLocalDb;Initial Catalog=MyAppTest;Integrated Security=True;" />
</connectionStrings>


متد DestroyDatabase 

private static void DestroyDatabase()
{
    var fileNames = ExecuteSqlQuery(Master, SqlResource.SelecDatabaseFileNames,
        row => (string)row["physical_name"]);

    if (!fileNames.Any()) return;

    ExecuteSqlCommand(Master, SqlResource.DetachDatabase);

    fileNames.ForEach(File.Delete);
}

در این متد ابتدا آدرس فایل‌های مرتبط با «دیتابیس تست» واکشی شده و در ادامه دستورات Detach دیتابیس انجام شده و فایل‌های مرتبط حذف خواهند شد. فایل‌های دیتابیس در مسیری شبیه به آدرس نشان داده شده‌ی در شکل زیر ذخیره خواهند شد.


قدم چهارم

برای جلوگیری از تداخل بین تست‌ها لازم است تک تک تست‌ها از هم ایزوله باشند؛ یکی از راه حل‌های موجود، استفاده از تراکنش‌ها می‌باشد. برای این منظور امکان AutoRollback را به صورت خودکار به متدهای تست با استفاده از Attribute زیر اعمال خواهیم کرد:

public class AutoRollbackAttribute : Attribute, ITestAction
{
    private TransactionScope _scope;

    public void BeforeTest(ITest test)
    {
        _scope = new TransactionScope(TransactionScopeOption.RequiresNew,new TransactionOptions {IsolationLevel = IsolationLevel.Snapshot});
    }

    public void AfterTest(ITest test)
    {
        _scope?.Dispose();
        _scope = null;
    }

    public ActionTargets Targets => ActionTargets.Test;
}

متدهای BeforTest و AfterTest به ترتیب قبل و بعد از اجرای متدهای تست تزئین شده با این Attribute اجرا خواهند شد. 


در مواقعی هم که به HttpConext نیاز دارید، می‌توانید از کتابخانه FakeHttpContext بهره ببرید. برای این مورد هم میتوان Attributeای را به مانند AutoRollback در نظر گرفت.

public class HttpContextAttribute:Attribute,ITestAction
{
    private FakeHttpContext.FakeHttpContext _httpContext;

    public void BeforeTest(ITest test)
    {
        _httpContext = new FakeHttpContext.FakeHttpContext();

    }

    public void AfterTest(ITest test)
    {
        _httpContext?.Dispose();
        _httpContext = null;
    }

    public ActionTargets Targets => ActionTargets.Test;
}

کاری که FakeHttpContext انجام می‌دهد، مقدار دهی HttpContext.Current با یک پیاده سازی ساختگی می‌باشد.


قدم پنجم

به عنوان مثال اگر بخواهیم برای سرویس «گروه کاربری»، Integration Test بنویسیم، به شکل زیر عمل خواهیم کرد:

namespace MyApp.IntegrationTests.ServiceLayer
{
    [TestFixture]
    [AutoRollback]
    [HttpContext]
    public class RoleServiceTests
    {
        private IRoleApplicationService _roleService;

        [SetUp]
        public void Init()
        {
        }

        [TearDown]
        public void Clean()
        {
        }

        [OneTimeSetUp]
        public void SetUp()
        {
            _roleService = IoC.Resolve<IRoleApplicationService>();

            using (var uow = IoC.Resolve<IUnitOfWork>())
            {
                RoleInitialDataBuilder.Build(uow);
            }
        }

        [OneTimeTearDown]
        public void TearDown()
        {
        }

        [Test]
        [TestCase("Role1")]
        public void Should_Create_New_Role(string role)
        {
            var viewModel = new RoleCreateViewModel
            {
                Name = role
            };

            _roleService.Create(viewModel);

            using (var context = IoC.Resolve<IUnitOfWork>())
            {
                var user = context.Set<Role>().FirstOrDefault(a => a.Name == role);
                user.ShouldNotBeNull();
            }
        }

        [Test]
        public void Should_Not_Create_New_Role_With_Admin_Name()
        {
            var viewModel = new RoleCreateViewModel
            {
                Name = "Admin"
            };

            Assert.Throws<DbUpdateException>(() => _roleService.Create(viewModel));
        }

        [Test]
        public void Should_AdminRole_Exists()
        {
            using (var context = IoC.Resolve<IUnitOfWork>())
            {
                var user = context.Set<Role>().FirstOrDefault(a => a.Name == "Admin");
                user.ShouldNotBeNull();
            }
        }

        [Test]
        public void Should_Not_Create_New_Role_Without_Name()
        {
            Assert.Throws<ValidationException>(() => _roleService.Create(new RoleCreateViewModel { Name = null }));
        }
    }
}

با این خروجی:



کدهای کامل این قسمت را می‌توانید از اینجا دریافت کنید. 
  • #
    ‫۶ سال و ۴ ماه قبل، شنبه ۲۹ اردیبهشت ۱۳۹۷، ساعت ۱۳:۲۷
    نکته تکمیلی
    در پروژه خود از الگوی Container Per Request استفاده می‌کنید؟ برای نزدیکتر کردن شرایط تست به شرایط محیط عملیاتی می‌توان به شکل زیر عمل کرد:
    کلاسی برای ایجاد و تخریب Nested Container 
        public static class TestDependencyScope
        {
            private static IContainer _currentNestedContainer;
    
            public static void Begin()
            {
                if (_currentNestedContainer != null)
                    throw new Exception("Cannot begin test dependency scope. Another dependency scope is still in effect.");
    
                _currentNestedContainer = IoC.Container.GetNestedContainer();
            }
    
            public static IContainer CurrentNestedContainer
            {
                get
                {
                    if (_currentNestedContainer == null)
                        throw new Exception($"Cannot access the {nameof(CurrentNestedContainer)}. There is no dependency scope in effect.");
    
                    return _currentNestedContainer;
                }
            }
    
            public static void End()
            {
                if (_currentNestedContainer == null)
                    throw new Exception("Cannot end test dependency scope. There is no dependency scope in effect.");
    
                _currentNestedContainer.Dispose();
                _currentNestedContainer = null;
            }
        }

    سپس به مانند  AutoRollbackAttrbiute مذکور در مطلب جاری، ContainerPerTestCaseAttribute را برای مدیریت این قضیه در نظر می‌گیریم:
     public class ContainerPerTestCaseAttribute : Attribute, ITestAction
        {   
            public void BeforeTest(ITest test)
            {
                TestDependencyScope.Begin();
            }
    
            public void AfterTest(ITest test)
            {
                TestDependencyScope.End();
            }
    
            public ActionTargets Targets => ActionTargets.Test;
        }
    و حال نحوه استفاده از آن:
        [PopulateHttpContext]
        [ContainerPerTestCase]
        [Transactional]
        [TestFixture]
        public class IntegratedTestBase
        {
            [SetUp]
            public void EachTestSetUp()
            {
                BeforeEachTest();
            }
            [TearDown]
            public void EachTestTearDown()
            {
                AfterEachTest();
            }
            protected virtual void BeforeEachTest()
            {
            }
            protected virtual void AfterEachTest()
            {
            }
    
            protected void UsingUnitOfWork(Action<IUnitOfWork> action)
            {
                IoC.Container.Using((IUnitOfWork uow)=>
                {
                    uow.DisableAllFilters();
                    action(uow);
                });
            }
    
            protected T UsingUnitOfWork<T>(Func<IUnitOfWork, T> func)
            {
                var uow = IoC.Resolve<IUnitOfWork>();
    
                uow.DisableAllFilters();
    
                using (uow)
                {
                    var result = func(uow);
    
                    uow.SaveChanges();
    
                    return result;
                }
            }
            protected async Task<T> UsingUnitOfWorkAsync<T>(Func<IUnitOfWork, Task<T>> func)
            {
                var uow = IoC.Resolve<IUnitOfWork>();
    
                uow.DisableAllFilters();
    
                using (uow)
                {
                    var result = await func(uow).ConfigureAwait(false);
    
                    await uow.SaveChangesAsync().ConfigureAwait(false);
    
                    return result;
                }
    
            }
        }
    و برای دسترسی به Nested Container جاری می‌توان به شکل زیر عمل کرد:
    namespace ProjectName.ServiceLayer.IntegrationTests
    {
        public static class Testing
        {
            private static IContainer Container => TestDependencyScope.CurrentNestedContainer;
    
            public static T Resolve<T>()
            {
                return Container.GetInstance<T>();
            }
    
            public static object Resolve(Type type)
            {
                return Container.GetInstance(type);
            }
    
            public static void Inject<T>(T instance) where T : class
            {
                Container.Inject(instance);
            }
        }
    }
    
    //in test classes
    using static ProjectName.ServiceLayer.IntegrationTests.Testing;
    namespace ProjectName.ServiceLayer.IntegrationTests
    {
        public class RoleServiceTests : IntegratedTestBase
        {
            private IRoleService _service;
            protected override void BeforeEachTest()
            {
                _service = Resolve<IRoleService>();
            }
         }
    }

  • #
    ‫۶ سال قبل، پنجشنبه ۲۲ شهریور ۱۳۹۷، ساعت ۰۴:۵۴
    معادل مطلب جاری برای EF Core

    برای آماده سازی دیتابیس واقعی به منظور تست جامعیت با EF Core می‌توان به شکل زیر عمل کرد:
    services.AddEntityFrameworkSqlServer()
                            .AddDbContext<ProjectNameDbContext>(builder =>
                                builder.UseSqlServer(
                                    $@"Data Source=(LocalDB)\MSSQLLocalDb;Initial Catalog=IntegrationTesting;Integrated Security=True;MultipleActiveResultSets=true;AttachDbFileName={FileName}"));
    
    
    private static string FileName => Path.Combine(
        Path.GetDirectoryName(
            typeof(TestingHelper).GetTypeInfo().Assembly.Location),
        "IntegrationTesting.mdf");
    و در نهایت برای ساخت دیتابیس قبل از اجرای تست ها، به شکل زیر می‌بایست عمل کرد:
    _serviceProvider.RunScopedService<ProjectNameDbContext>(context =>
    {
        context.Database.EnsureDeleted();
        context.Database.EnsureCreated();
    });

  • #
    ‫۴ سال و ۳ ماه قبل، جمعه ۲ خرداد ۱۳۹۹، ساعت ۱۸:۵۱
    فرق بین MSTest و NUnitTest و XUnitTest در چیه؟

    چرا برای نوشتن تست لایه‌های مختلف جدا در نظر میگیرید؟ مثالا من برای کنترلرها تست می‌نویسم و به نظرم با اینکار کل وابستگی‌ها تست میشه.(البته همیشه با HttpContext z مشکل داشتم برای تست.)