유닛 테스트 ASP.NET Web API 2 Entity Framework 모의Mocking Entity Framework when Unit Testing ASP.NET Web API 2

만든 사람 Tom FitzMackenby Tom FitzMacken

완료 된 프로젝트 다운로드Download Completed Project

이 지침 및 응용 프로그램에서는 Entity Framework를 사용 하는 Web API 2 응용 프로그램에 대 한 단위 테스트를 만드는 방법을 보여 줍니다.This guidance and application demonstrate how to create unit tests for your Web API 2 application that uses the Entity Framework. 스 캐 폴드 컨트롤러를 수정 하 여 테스트를 위해 컨텍스트 개체를 전달할 수 있도록 하는 방법과 Entity Framework 작업 하는 테스트 개체를 만드는 방법을 보여 줍니다.It shows how to modify the scaffolded controller to enable passing a context object for testing, and how to create test objects that work with Entity Framework.

ASP.NET Web API를 사용한 단위 테스트에 대 한 소개는 ASP.NET Web API 2를 사용한 단위 테스트를 참조 하세요.For an introduction to unit testing with ASP.NET Web API, see Unit Testing with ASP.NET Web API 2.

이 자습서에서는 사용자가 ASP.NET Web API의 기본 개념을 잘 알고 있다고 가정 합니다.This tutorial assumes you are familiar with the basic concepts of ASP.NET Web API. 소개 자습서는 ASP.NET Web API 2 시작을 참조 하세요.For an introductory tutorial, see Getting Started with ASP.NET Web API 2.

자습서에서 사용 되는 소프트웨어 버전Software versions used in the tutorial

항목 내용In this topic

이 항목에는 다음과 같은 섹션이 포함되어 있습니다.This topic contains the following sections:

ASP.NET Web API 2를 사용 하 여 단위 테스트의 단계를 이미 완료 한 경우에는 컨트롤러 추가섹션으로 건너뛸 수 있습니다.If you have already completed the steps in Unit Testing with ASP.NET Web API 2, you can skip to the section Add the controller.

사전 요구 사항Prerequisites

Visual Studio 2017 Community, Professional 또는 Enterprise editionVisual Studio 2017 Community, Professional or Enterprise edition

코드 다운로드Download code

완료 된 프로젝트를 다운로드 합니다.Download the completed project. 다운로드 가능한 프로젝트에는이 항목에 대 한 단위 테스트 코드와 단위 테스트 ASP.NET Web API 2 항목 항목이 포함 되어 있습니다.The downloadable project includes unit test code for this topic and for the Unit Testing ASP.NET Web API 2 topic.

단위 테스트 프로젝트를 사용 하 여 응용 프로그램 만들기Create application with unit test project

응용 프로그램을 만들거나 기존 응용 프로그램에 단위 테스트 프로젝트를 추가할 때 단위 테스트 프로젝트를 만들 수 있습니다.You can either create a unit test project when creating your application or add a unit test project to an existing application. 이 자습서에서는 응용 프로그램을 만들 때 단위 테스트 프로젝트를 만드는 방법을 보여 줍니다.This tutorial shows creating a unit test project when creating the application.

ASP.NET app이라는 새 웹 응용 프로그램을 만듭니다.Create a new ASP.NET Web Application named StoreApp.

새 ASP.NET 프로젝트 창에서 템플릿을 선택 하 고 Web API에 대 한 폴더 및 핵심 참조를 추가 합니다.In the New ASP.NET Project windows, select the Empty template and add folders and core references for Web API. 단위 테스트 추가 옵션을 선택 합니다.Select the Add unit tests option. 단위 테스트 프로젝트는 자동 으로 이름이로 지정 됩니다.The unit test project is automatically named StoreApp.Tests. 이 이름을 유지할 수 있습니다.You can keep this name.

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

응용 프로그램을 만든 후에는 해당 응용 프로그램에 두 개의 프로젝트를 포함 하는 것을 볼 수 있습니다.After creating the application, you will see it contains two projects - StoreApp and StoreApp.Tests.

모델 클래스 만들기Create the model class

사용자 응용 프로그램 프로젝트에서 Product.cs라는 모델 폴더에 클래스 파일을 추가 합니다.In your StoreApp project, add a class file to the Models folder named Product.cs. 파일 내용을 다음 코드로 바꿉니다.Replace the contents of the file with the following code.

using System;

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

솔루션을 빌드합니다.Build the solution.

컨트롤러 추가Add the controller

Controllers 폴더를 마우스 오른쪽 단추로 클릭 하 고 추가새 스 캐 폴드 항목을 선택 합니다.Right-click the Controllers folder and select Add and New Scaffolded Item. Entity Framework를 사용 하 여 작업을 포함 하는 Web API 2 컨트롤러를 선택 합니다.Select Web API 2 Controller with actions, using Entity Framework.

새 컨트롤러 추가

다음 값을 설정합니다.Set the following values:

  • 컨트롤러 이름: 제품 컨트롤러Controller name: ProductController
  • 모델 클래스: ProductModel class: Product
  • 데이터 컨텍스트 클래스: [아래에 표시 된 값을 채우는 새 데이터 컨텍스트 단추 선택]Data context class: [Select New data context button which fills in the values seen below]

컨트롤러 지정

추가 를 클릭 하 여 자동으로 생성 된 코드를 사용 하 여 컨트롤러를 만듭니다.Click Add to create the controller with automatically-generated code. 이 코드에는 Product 클래스의 인스턴스를 만들고, 검색 하 고, 업데이트 하 고, 삭제 하는 메서드가 포함 되어 있습니다.The code includes methods for creating, retrieving, updating and deleting instances of the Product class. 다음 코드는 제품을 추가 하는 메서드를 보여 줍니다.The following code shows the method for add a Product. 이 메서드는 IHttpActionResult의 인스턴스를 반환 합니다.Notice that the method returns an instance of IHttpActionResult.

// 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의 새로운 기능 중 하나로, 단위 테스트 개발을 간소화 합니다.IHttpActionResult is one of the new features in Web API 2, and it simplifies unit test development.

다음 섹션에서는 테스트 개체를 컨트롤러에 전달 하는 데 도움이 되도록 생성 된 코드를 사용자 지정 합니다.In the next section, you will customize the generated code to facilitate passing test objects to the controller.

종속성 주입 추가Add dependency injection

현재, 제품 컨트롤러 클래스는 하드 코드 되어 있으므로이 클래스의 인스턴스를 사용할 수 있습니다.Currently, the ProductController class is hard-coded to use an instance of the StoreAppContext class. 종속성 주입 이라고 하는 패턴을 사용 하 여 응용 프로그램을 수정 하 고 하드 코드 된 종속성을 제거 합니다.You will use a pattern called dependency injection to modify your application and remove that hard-coded dependency. 이 종속성을 위반 하면 테스트할 때 모의 개체를 전달할 수 있습니다.By breaking this dependency, you can pass in a mock object when testing.

모델 폴더를 마우스 오른쪽 단추로 클릭 하 고 IStoreAppContext이라는 새 인터페이스를 추가 합니다.Right-click the Models folder, and add a new interface named IStoreAppContext.

코드를 다음 코드로 바꿉니다.Replace the code with the following code.

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 파일을 열고 다음과 같이 강조 표시를 변경 합니다.Open the StoreAppContext.cs file and make the following highlighted changes. 유의 해야 할 중요 한 사항은 다음과 같습니다.The important changes to note are:

  • IStoreAppContext Appcontext 클래스는 인터페이스를 구현 합니다.StoreAppContext class implements IStoreAppContext interface
  • MarkAsModified 메서드를 구현 합니다.MarkAsModified method is implemented
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 파일을 엽니다.Open the ProductController.cs file. 강조 표시 된 코드와 일치 하도록 기존 코드를 변경 합니다.Change the existing code to match the highlighted code. 이러한 변경 내용으로 인해에는가 나 Appcontext에 대 한 종속성이 중단 되 고 다른 클래스가 컨텍스트 클래스에 대해 다른 개체를 전달할 수 있습니다.These changes break the dependency on StoreAppContext and enable other classes to pass in a different object for the context class. 이렇게 변경 하면 단위 테스트 중에 테스트 컨텍스트를 전달할 수 있습니다.This change will enable you to pass in a test context during unit tests.

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
}

제품 컨트롤러에서 수행 해야 하는 변경 내용이 하나 더 있습니다.There is one more change you must make in ProductController. Putproduct 메서드에서 엔터티 상태를 설정 하는 줄을 MarkAsModified 메서드에 대 한 호출로 바꿉니다.In the PutProduct method, replace the line that sets the entity state to modified with a call to the MarkAsModified method.

// 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
}

솔루션을 빌드합니다.Build the solution.

이제 테스트 프로젝트를 설정할 준비가 되었습니다.You are now ready to set up the test project.

테스트 프로젝트에 NuGet 패키지 설치Install NuGet packages in test project

빈 템플릿을 사용 하 여 응용 프로그램을 만드는 경우 단위 테스트 프로젝트 (사용자 응용 프로그램 테스트)에는 설치 된 NuGet 패키지가 포함 되지 않습니다.When you use the Empty template to create an application, the unit test project (StoreApp.Tests) does not include any installed NuGet packages. Web API 템플릿과 같은 다른 템플릿에는 단위 테스트 프로젝트의 일부 NuGet 패키지가 포함 됩니다.Other templates, such as the Web API template, include some NuGet packages in the unit test project. 이 자습서에서는 Entity Framework 패키지와 Microsoft ASP.NET Web API 2 Core 패키지를 테스트 프로젝트에 포함 해야 합니다.For this tutorial, you must include the Entity Framework package and the Microsoft ASP.NET Web API 2 Core package to the test project.

마우스 오른쪽 단추를 클릭 하 여 프로젝트를 마우스 오른쪽 단추로 클릭 하 고 NuGet 패키지 관리를 선택 합니다.Right-click the StoreApp.Tests project and select Manage NuGet Packages. 해당 프로젝트에 패키지를 추가 하려면 사용자 응용 프로그램. 테스트 프로젝트를 선택 해야 합니다.You must select the StoreApp.Tests project to add the packages to that project.

패키지 관리

온라인 패키지에서 EntityFramework 패키지 (버전 6.0 이상)를 찾아서 설치 합니다.From the Online packages, find and install the EntityFramework package (version 6.0 or later). EntityFramework 패키지가 이미 설치 된 것으로 나타나는 경우에는 사용자 응용 프로그램 프로젝트 대신 파일 앱 프로젝트를 선택 했을 수 있습니다.If it appears that the EntityFramework package is already installed, you may have selected the StoreApp project instead of the StoreApp.Tests project.

add Entity Framework

Microsoft ASP.NET Web API 2 핵심 패키지를 찾아서 설치 합니다.Find and install Microsoft ASP.NET Web API 2 Core package.

web api core 패키지 설치

NuGet 패키지 관리 창을 닫습니다.Close the Manage NuGet Packages window.

테스트 컨텍스트 만들기Create test context

Testdbset 이라는 클래스를 테스트 프로젝트에 추가 합니다.Add a class named TestDbSet to the test project. 이 클래스는 테스트 데이터 집합에 대 한 기본 클래스로 사용 됩니다.This class serves as the base class for your test data set. 코드를 다음 코드로 바꿉니다.Replace the code with the following code.

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 이라는 클래스를 추가 합니다.Add a class named TestProductDbSet to the test project which contains the following code.

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 라는 클래스를 추가 하 고 기존 코드를 다음 코드로 바꿉니다.Add a class named TestStoreAppContext and replace the existing code with the following code.

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() { }
    }
}

테스트 만들기Create tests

기본적으로 테스트 프로젝트에는 UnitTest1.cs라는 빈 테스트 파일이 포함 되어 있습니다.By default, your test project includes an empty test file named UnitTest1.cs. 이 파일은 테스트 메서드를 만드는 데 사용 하는 특성을 보여 줍니다.This file shows the attributes you use to create test methods. 이 자습서에서는 새 테스트 클래스를 추가 하기 때문에이 파일을 삭제할 수 있습니다.For this tutorial, you can delete this file because you will add a new test class.

Test제품 컨트롤러 라는 클래스를 테스트 프로젝트에 추가 합니다.Add a class named TestProductController to the test project. 코드를 다음 코드로 바꿉니다.Replace the code with the following code.

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 };
        }
    }
}

테스트 실행Run tests

이제 테스트를 실행할 준비가 되었습니다.You are now ready to run the tests. TestMethod 특성으로 표시 된 모든 메서드는 테스트 됩니다.All of the method that are marked with the TestMethod attribute will be tested. 테스트 메뉴 항목에서 테스트를 실행 합니다.From the Test menu item, run the tests.

테스트 실행

테스트 탐색기 창을 열고 테스트 결과를 확인 합니다.Open the Test Explorer window, and notice the results of the tests.

테스트 결과