단위 테스트 ASP.NET Web API 2일 때 Entity Framework 모의

Tom FitzMacken

완료된 프로젝트 다운로드

이 지침 및 애플리케이션은 Entity Framework를 사용하는 Web API 2 애플리케이션에 대한 단위 테스트를 만드는 방법을 보여 줍니다. 테스트를 위해 컨텍스트 개체를 전달할 수 있도록 스캐폴드된 컨트롤러를 수정하는 방법과 Entity Framework에서 작동하는 테스트 개체를 만드는 방법을 보여 줍니다.

ASP.NET Web API 단위 테스트에 대한 소개는 ASP.NET Web API 2를 사용한 유닛 테스트를 참조하세요.

이 자습서에서는 ASP.NET Web API 기본 개념을 잘 알고 있다고 가정합니다. 소개 자습서는 ASP.NET Web API 2의 시작 참조하세요.

자습서에서 사용되는 소프트웨어 버전

항목 내용

이 항목에는 다음과 같은 섹션이 포함되어 있습니다.

ASP.NET Web API 2를 사용하여 유닛 테스트의 단계를 이미 완료한 경우 컨트롤러 추가 섹션으로 건너뛸 수 있습니다.

사전 요구 사항

Visual Studio 2017 Community, Professional 또는 Enterprise Edition

코드 다운로드

완료된 프로젝트를 다운로드합니다. 다운로드 가능한 프로젝트에는 이 항목 및 유닛 테스트 ASP.NET Web API 2 항목에 대한 단위 테스트 코드가 포함되어 있습니다.

단위 테스트 프로젝트를 사용하여 애플리케이션 만들기

애플리케이션을 만들 때 단위 테스트 프로젝트를 만들거나 기존 애플리케이션에 단위 테스트 프로젝트를 추가할 수 있습니다. 이 자습서에서는 애플리케이션을 만들 때 단위 테스트 프로젝트를 만드는 방법을 보여줍니다.

StoreApp이라는 새 ASP.NET 웹 애플리케이션을 만듭니다.

새 ASP.NET 프로젝트 창에서 템플릿을 선택하고 Web API에 대한 폴더 및 핵심 참조를 추가합니다. 단위 테스트 추가 옵션을 선택합니다. 단위 테스트 프로젝트의 이름은 자동으로 StoreApp.Tests입니다. 이 이름을 유지할 수 있습니다.

단위 테스트 프로젝트 만들기

애플리케이션을 만든 후 StoreApp 및 StoreApp.Tests의 두 프로젝트가 포함된 것을 볼 수 있습니다.

모델 클래스 만들기

StoreApp 프로젝트에서 Product.cs라는 Models 폴더에 클래스 파일을 추가합니다. 파일 내용을 다음 코드로 바꿉니다.

using System;

namespace StoreApp.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

솔루션을 빌드합니다.

컨트롤러 추가

Controllers 폴더를 마우스 오른쪽 단추로 클릭하고 추가새 스캐폴드된 항목을 선택합니다. Entity Framework를 사용하여 작업이 있는 Web API 2 컨트롤러를 선택합니다.

새 컨트롤러 추가

다음 값을 설정합니다.

  • 컨트롤러 이름: ProductController
  • 모델 클래스: Product
  • 데이터 컨텍스트 클래스: [아래에 표시된 값을 채우는 새 데이터 컨텍스트 선택 단추]

컨트롤러 지정

추가를 클릭하여 자동으로 생성된 코드를 사용하여 컨트롤러를 만듭니다. 코드에는 Product 클래스의 인스턴스를 만들고, 검색하고, 업데이트하고, 삭제하는 메서드가 포함되어 있습니다. 다음 코드에서는 Product를 추가하는 방법을 보여 있습니다. 메서드는 IHttpActionResult의 instance 반환합니다.

// POST api/Product
[ResponseType(typeof(Product))]
public IHttpActionResult PostProduct(Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    db.Products.Add(product);
    db.SaveChanges();

    return CreatedAtRoute("DefaultApi", new { id = product.Id }, product);
}

IHttpActionResult는 Web API 2의 새로운 기능 중 하나이며 단위 테스트 개발을 간소화합니다.

다음 섹션에서는 컨트롤러에 테스트 개체를 쉽게 전달할 수 있도록 생성된 코드를 사용자 지정합니다.

종속성 주입 추가

현재 ProductController 클래스는 StoreAppContext 클래스의 instance 사용하도록 하드 코딩됩니다. 종속성 주입이라는 패턴을 사용하여 애플리케이션을 수정하고 하드 코딩된 종속성을 제거합니다. 이 종속성을 중단하면 테스트할 때 모의 개체를 전달할 수 있습니다.

Models 폴더를 마우스 오른쪽 단추로 클릭하고 IStoreAppContext라는 새 인터페이스를 추가합니다.

코드를 다음 코드로 바꿉니다.

using System;
using System.Data.Entity;

namespace StoreApp.Models
{
    public interface IStoreAppContext : IDisposable
    {
        DbSet<Product> Products { get; }
        int SaveChanges();
        void MarkAsModified(Product item);    
    }
}

StoreAppContext.cs 파일을 열고 다음과 같은 강조 표시된 변경 내용을 적용합니다. 주의해야 할 중요한 변경 사항은 다음과 같습니다.

  • StoreAppContext 클래스는 IStoreAppContext 인터페이스를 구현합니다.
  • MarkAsModified 메서드가 구현됨
using System;
using System.Data.Entity;

namespace StoreApp.Models
{
    public class StoreAppContext : DbContext, IStoreAppContext
    {
        public StoreAppContext() : base("name=StoreAppContext")
        {
        }

        public DbSet<Product> Products { get; set; }
    
        public void MarkAsModified(Product item)
        {
            Entry(item).State = EntityState.Modified;
        }
    }
}

ProductController.cs 파일을 엽니다. 강조 표시된 코드와 일치하도록 기존 코드를 변경합니다. 이러한 변경은 StoreAppContext에 대한 종속성을 깨고 다른 클래스가 컨텍스트 클래스에 대해 다른 개체를 전달할 수 있도록 합니다. 이렇게 변경하면 단위 테스트 중에 테스트 컨텍스트를 전달할 수 있습니다.

public class ProductController : ApiController
{
    // modify the type of the db field
    private IStoreAppContext db = new StoreAppContext();

    // add these constructors
    public ProductController() { }

    public ProductController(IStoreAppContext context)
    {
        db = context;
    }
    // rest of class not shown
}

ProductController에서 변경해야 하는 변경 사항이 하나 더 있습니다. PutProduct 메서드에서 수정할 엔터티 상태를 설정하는 줄을 MarkAsModified 메서드 호출로 바꿉다.

// PUT api/Product/5
public IHttpActionResult PutProduct(int id, Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    if (id != product.Id)
    {
        return BadRequest();
    }

    //db.Entry(product).State = EntityState.Modified;
    db.MarkAsModified(product);
    
    // rest of method not shown
}

솔루션을 빌드합니다.

이제 테스트 프로젝트를 설정할 준비가 되었습니다.

테스트 프로젝트에 NuGet 패키지 설치

빈 템플릿을 사용하여 애플리케이션을 만드는 경우 단위 테스트 프로젝트(StoreApp.Tests)에 설치된 NuGet 패키지가 포함되지 않습니다. Web API 템플릿과 같은 다른 템플릿에는 단위 테스트 프로젝트에 일부 NuGet 패키지가 포함됩니다. 이 자습서에서는 Entity Framework 패키지와 Microsoft ASP.NET Web API 2 Core 패키지를 테스트 프로젝트에 포함해야 합니다.

StoreApp.Tests 프로젝트를 마우스 오른쪽 단추로 클릭하고 NuGet 패키지 관리를 선택합니다. 해당 프로젝트에 패키지를 추가하려면 StoreApp.Tests 프로젝트를 선택해야 합니다.

패키지 관리

온라인 패키지에서 EntityFramework 패키지(버전 6.0 이상)를 찾아 설치합니다. EntityFramework 패키지가 이미 설치된 것으로 표시되는 경우 StoreApp.Tests 프로젝트 대신 StoreApp 프로젝트를 선택했을 수 있습니다.

Entity Framework 추가

Microsoft ASP.NET Web API 2 Core 패키지를 찾아 설치합니다.

웹 api Core 패키지 설치

NuGet 패키지 관리 창을 닫습니다.

테스트 컨텍스트 만들기

TestDbSet이라는 클래스를 테스트 프로젝트에 추가합니다. 이 클래스는 테스트 데이터 집합의 기본 클래스 역할을 합니다. 코드를 다음 코드로 바꿉니다.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Entity;
using System.Linq;

namespace StoreApp.Tests
{
    public class TestDbSet<T> : DbSet<T>, IQueryable, IEnumerable<T>
        where T : class
    {
        ObservableCollection<T> _data;
        IQueryable _query;

        public TestDbSet()
        {
            _data = new ObservableCollection<T>();
            _query = _data.AsQueryable();
        }

        public override T Add(T item)
        {
            _data.Add(item);
            return item;
        }

        public override T Remove(T item)
        {
            _data.Remove(item);
            return item;
        }

        public override T Attach(T item)
        {
            _data.Add(item);
            return item;
        }

        public override T Create()
        {
            return Activator.CreateInstance<T>();
        }

        public override TDerivedEntity Create<TDerivedEntity>()
        {
            return Activator.CreateInstance<TDerivedEntity>();
        }

        public override ObservableCollection<T> Local
        {
            get { return new ObservableCollection<T>(_data); }
        }

        Type IQueryable.ElementType
        {
            get { return _query.ElementType; }
        }

        System.Linq.Expressions.Expression IQueryable.Expression
        {
            get { return _query.Expression; }
        }

        IQueryProvider IQueryable.Provider
        {
            get { return _query.Provider; }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return _data.GetEnumerator();
        }

        IEnumerator<T> IEnumerable<T>.GetEnumerator()
        {
            return _data.GetEnumerator();
        }
    }
}

다음 코드가 포함된 테스트 프로젝트에 TestProductDbSet 이라는 클래스를 추가합니다.

using System;
using System.Linq;
using StoreApp.Models;

namespace StoreApp.Tests
{
    class TestProductDbSet : TestDbSet<Product>
    {
        public override Product Find(params object[] keyValues)
        {
            return this.SingleOrDefault(product => product.Id == (int)keyValues.Single());
        }
    }
}

TestStoreAppContext라는 클래스를 추가하고 기존 코드를 다음 코드로 바꿉다.

using System;
using System.Data.Entity;
using StoreApp.Models;

namespace StoreApp.Tests
{
    public class TestStoreAppContext : IStoreAppContext 
    {
        public TestStoreAppContext()
        {
            this.Products = new TestProductDbSet();
        }

        public DbSet<Product> Products { get; set; }

        public int SaveChanges()
        {
            return 0;
        }

        public void MarkAsModified(Product item) { }
        public void Dispose() { }
    }
}

테스트 만들기

기본적으로 테스트 프로젝트에는 UnitTest1.cs라는 빈 테스트 파일이 포함됩니다. 이 파일은 테스트 메서드를 만드는 데 사용하는 특성을 보여 줍니다. 이 자습서에서는 새 테스트 클래스를 추가하므로 이 파일을 삭제할 수 있습니다.

TestProductController라는 클래스를 테스트 프로젝트에 추가합니다. 코드를 다음 코드로 바꿉니다.

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Web.Http.Results;
using System.Net;
using StoreApp.Models;
using StoreApp.Controllers;

namespace StoreApp.Tests
{
    [TestClass]
    public class TestProductController
    {
        [TestMethod]
        public void PostProduct_ShouldReturnSameProduct()
        {
            var controller = new ProductController(new TestStoreAppContext());

            var item = GetDemoProduct();

            var result =
                controller.PostProduct(item) as CreatedAtRouteNegotiatedContentResult<Product>;

            Assert.IsNotNull(result);
            Assert.AreEqual(result.RouteName, "DefaultApi");
            Assert.AreEqual(result.RouteValues["id"], result.Content.Id);
            Assert.AreEqual(result.Content.Name, item.Name);
        }

        [TestMethod]
        public void PutProduct_ShouldReturnStatusCode()
        {
            var controller = new ProductController(new TestStoreAppContext());

            var item = GetDemoProduct();

            var result = controller.PutProduct(item.Id, item) as StatusCodeResult;
            Assert.IsNotNull(result);
            Assert.IsInstanceOfType(result, typeof(StatusCodeResult));
            Assert.AreEqual(HttpStatusCode.NoContent, result.StatusCode);
        }

        [TestMethod]
        public void PutProduct_ShouldFail_WhenDifferentID()
        {
            var controller = new ProductController(new TestStoreAppContext());

            var badresult = controller.PutProduct(999, GetDemoProduct());
            Assert.IsInstanceOfType(badresult, typeof(BadRequestResult));
        }

        [TestMethod]
        public void GetProduct_ShouldReturnProductWithSameID()
        {
            var context = new TestStoreAppContext();
            context.Products.Add(GetDemoProduct());

            var controller = new ProductController(context);
            var result = controller.GetProduct(3) as OkNegotiatedContentResult<Product>;

            Assert.IsNotNull(result);
            Assert.AreEqual(3, result.Content.Id);
        }

        [TestMethod]
        public void GetProducts_ShouldReturnAllProducts()
        {
            var context = new TestStoreAppContext();
            context.Products.Add(new Product { Id = 1, Name = "Demo1", Price = 20 });
            context.Products.Add(new Product { Id = 2, Name = "Demo2", Price = 30 });
            context.Products.Add(new Product { Id = 3, Name = "Demo3", Price = 40 });

            var controller = new ProductController(context);
            var result = controller.GetProducts() as TestProductDbSet;

            Assert.IsNotNull(result);
            Assert.AreEqual(3, result.Local.Count);
        }

        [TestMethod]
        public void DeleteProduct_ShouldReturnOK()
        {
            var context = new TestStoreAppContext();
            var item = GetDemoProduct();
            context.Products.Add(item);

            var controller = new ProductController(context);
            var result = controller.DeleteProduct(3) as OkNegotiatedContentResult<Product>;

            Assert.IsNotNull(result);
            Assert.AreEqual(item.Id, result.Content.Id);
        }

        Product GetDemoProduct()
        {
            return new Product() { Id = 3, Name = "Demo name", Price = 5 };
        }
    }
}

테스트 실행

이제 테스트를 실행할 준비가 되었습니다. TestMethod 특성으로 표시된 모든 메서드가 테스트됩니다. 테스트 메뉴 항목에서 테스트를 실행합니다.

테스트 실행

테스트 Explorer 창을 열고 테스트 결과를 확인합니다.

테스트 결과