ASP.Net Core Web API、EF Core、SQLLite整合測試

整合測試

上述自己需求要做Side Project要做一些整合測試,SQL Lite向來是一個省錢方案的做法。

需安裝套件

  1. Microsoft.EntityFrameworkCore
  2. Microsoft.EntityFrameworkCore.SqlLite
  3. Microsoft.EntityFrameworkCore.Tools

快速建構API

按照先前做法新增一個Model/Category.cs

  public class Category
  {
      public int Id { get; set; }
      public string Name { get; set; }
      public string Description { get; set; }
  }
    public class CategoryDbContext:DbContext
    {
        public CategoryDbContext(DbContextOptions<CategoryDbContext> options) : base(options)
        {
        }

        public DbSet<Category> Category { get; set; }
    }
using CategoryAPIDemo2.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace CategoryAPIDemo2.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CategoryController : ControllerBase
    {
        private readonly CategoryDbContext _context;

        public CategoryController(CategoryDbContext context)
        {
            _context = context;
        }

        [HttpGet]
        public async Task<ActionResult<IEnumerable<Category>>> GetCategorys()
        {
            return await _context.Category.ToListAsync();
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<Category>> GetCategory(int id)
        {
            var category = await _context.Category.FindAsync(id);

            if (category == null)
            {
                return NotFound();
            }

            return category;
        }

        [HttpPut("{id}")]
        public async Task<IActionResult> PutCategory(int id, Category category)
        {
            if (id != category.Id)
            {
                return BadRequest();
            }

            _context.Entry(category).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
                return CreatedAtAction("GetCategory", new { id = category.Id }, category);
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!CategoryExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }

       [HttpPost]
        public async Task<ActionResult<Category>> PostCategory(Category category)
        {
            _context.Category.Add(category);
            await _context.SaveChangesAsync();

            return CreatedAtAction("GetCategory", new { id = category.Id }, category);
        }


        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteCategory(int id)
        {
            var category = await _context.Category.FindAsync(id);
            if (category == null)
            {
                return NotFound();
            }

            _context.Category.Remove(category);
            await _context.SaveChangesAsync();

            return NoContent();
        }

        private bool CategoryExists(int id)
        {
            return _context.Category.Any(e => e.Id == id);
        }
    }
}

appSetings.json

  "ConnectionStrings": {
    "DefaultConnection": "Data Source=CategoryAPI.db"
  },

在Program.cs設定

using CategoryAPIDemo2.Models;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
builder.Services.AddDbContext<CategoryDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
 
public partial class Program { } // 為了 WebApplicationFactory 以便在測試專案當中能夠使用Program

資料移轉指令

enable-migrations

add-migration initCreate

update-database

實作整合測試專案XUnit

安裝所需套件

  1. Microsoft.AspNetCore.Mvc.Testing
  2. Microsoft.EntityFrameworkCore.InMemory
  3. Microsoft.EntityFrameworkCore.Sqlite

實作類別測試

using CategoryAPIDemo2.Models;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestPlatform.TestHost;
using System;
using System.Net.Http.Json;

namespace CategoryAPIDemo2Tests
{
    public class CategoryControllerTests : IClassFixture<WebApplicationFactory<Program>>
    {
        private readonly WebApplicationFactory<Program> _factory;

        public CategoryControllerTests(WebApplicationFactory<Program> factory)
        {
            _factory = factory.WithWebHostBuilder(builder =>
            {
                builder.ConfigureServices(services =>
                {
                    // 使用內存資料庫替代真實資料庫
                    var descriptor = services.SingleOrDefault(
                        d => d.ServiceType == typeof(DbContextOptions<CategoryDbContext>));

                    if (descriptor != null)
                    {
                        services.Remove(descriptor);
                    }

                    services.AddDbContext<CategoryDbContext>(options =>
                    {
                        options.UseInMemoryDatabase("InMemoryDbForTesting");
                    });

                    // 確保資料庫被建立
                    var sp = services.BuildServiceProvider();

                    using (var scope = sp.CreateScope())
                    {
                        var scopedServices = scope.ServiceProvider;
                        var db = scopedServices.GetRequiredService<CategoryDbContext>();

                        db.Database.EnsureCreated();

                        // 這裡可以初始化一些測試資料
                        db.Category.AddRange(
                            new Category { Name = "test1", Description = "test1" },
                            new Category { Name = "test2", Description ="test2" });
                        db.SaveChanges();
                    }
                });
            });
        }

        [Fact]
        public async Task GetAllCategorys_ReturnsSuccess()
        {
            // Arrange
            var client = _factory.CreateClient();

            // Act
            var response = await client.GetAsync("/api/Category");

            // Assert
            response.EnsureSuccessStatusCode(); // Status Code 200-299

            var categorys = await response.Content.ReadFromJsonAsync<IEnumerable<Category>>();
            Assert.NotNull(categorys);
            Assert.NotEmpty(categorys);  // 因為我們初始化了2個產品
        }

        [Fact]
        public async Task CreateCategory_ReturnsCreatedCategory()
        {
            // Arrange
            var client = _factory.CreateClient();
            var newCategory = new Category { Name = "test3",  Description="test3" };

            // Act
            var response = await client.PostAsJsonAsync("/api/category", newCategory);

            // Assert
            response.EnsureSuccessStatusCode();  // Status Code 201

            var category = await response.Content.ReadFromJsonAsync<Category>();
            Assert.NotNull(category);
            Assert.Equal("test3", category.Name);
        }

        [Fact]
        public async Task UpdateCategory_ReturnsUpdatedCategory()
        {
            // Arrange
            var client = _factory.CreateClient();
            // 假設我們要更新 ID = 1 的類別
            var updateCategory = new Category { Id = 1, Name = "UpdatedTest1", Description = "UpdatedDescription1" };
            // Act
            var response = await client.PutAsJsonAsync($"/api/category/{updateCategory.Id}", updateCategory);
            // Assert
            response.EnsureSuccessStatusCode();  // Status Code 200-299
            var updatedCategory = await response.Content.ReadFromJsonAsync<Category>();
            Assert.NotNull(updatedCategory);
            Assert.Equal("UpdatedTest1", updatedCategory.Name);
            Assert.Equal("UpdatedDescription1", updatedCategory.Description);
        }
        [Fact]
        public async Task DeleteCategory_ReturnsNoContent()
        {
            // Arrange
            var client = _factory.CreateClient();
            // 假設我們要刪除 ID = 2 的類別
            var categoryId = 2;
            // Act
            var response = await client.DeleteAsync($"/api/category/{categoryId}");
            // Assert
            response.EnsureSuccessStatusCode();  // Status Code 204

            // 嘗試查找剛剛刪除的類別
            var getResponse = await client.GetAsync($"/api/category/{categoryId}");
            Assert.Equal(System.Net.HttpStatusCode.NotFound, getResponse.StatusCode);  // 確保返回 404,表示找不到該類別
        }
    }
}

改寫用Fluent Assertions 

using CategoryAPIDemo2.Models;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestPlatform.TestHost;
using System.Net.Http.Json;
using FluentAssertions;

namespace CategoryAPIDemo2Tests
{
    public class CategoryControllerTests : IClassFixture<WebApplicationFactory<Program>>
    {
        private readonly WebApplicationFactory<Program> _factory;

        public CategoryControllerTests(WebApplicationFactory<Program> factory)
        {
            _factory = factory.WithWebHostBuilder(builder =>
            {
                builder.ConfigureServices(services =>
                {
                    // 使用內存資料庫替代真實資料庫
                    var descriptor = services.SingleOrDefault(
                        d => d.ServiceType == typeof(DbContextOptions<CategoryDbContext>));

                    if (descriptor != null)
                    {
                        services.Remove(descriptor);
                    }

                    services.AddDbContext<CategoryDbContext>(options =>
                    {
                        options.UseInMemoryDatabase("InMemoryDbForTesting");
                    });

                    // 確保資料庫被建立
                    var sp = services.BuildServiceProvider();

                    using (var scope = sp.CreateScope())
                    {
                        var scopedServices = scope.ServiceProvider;
                        var db = scopedServices.GetRequiredService<CategoryDbContext>();

                        db.Database.EnsureCreated();

                        // 初始化測試資料
                        db.Category.AddRange(
                            new Category { Name = "test1", Description = "test1" },
                            new Category { Name = "test2", Description = "test2" });
                        db.SaveChanges();
                    }
                });
            });
        }

        [Fact]
        public async Task GetAllCategorys_ReturnsSuccess()
        {
            // Arrange
            var client = _factory.CreateClient();

            // Act
            var response = await client.GetAsync("/api/Category");

            // Assert
            response.IsSuccessStatusCode.Should().BeTrue(); // 確認狀態碼 200-299
            var categories = await response.Content.ReadFromJsonAsync<IEnumerable<Category>>();
            categories.Should().NotBeNullOrEmpty(); // 因為我們初始化了2個產品
        }

        [Fact]
        public async Task CreateCategory_ReturnsCreatedCategory()
        {
            // Arrange
            var client = _factory.CreateClient();
            var newCategory = new Category { Name = "test3", Description = "test3" };

            // Act
            var response = await client.PostAsJsonAsync("/api/category", newCategory);

            // Assert
            response.StatusCode.Should().Be(System.Net.HttpStatusCode.Created); // 確認狀態碼 201

            var createdCategory = await response.Content.ReadFromJsonAsync<Category>();
            createdCategory.Should().NotBeNull();
            createdCategory.Name.Should().Be("test3");
            createdCategory.Description.Should().Be("test3");
        }

        [Fact]
        public async Task UpdateCategory_ReturnsUpdatedCategory()
        {
            // Arrange
            var client = _factory.CreateClient();
            var updateCategory = new Category { Id = 1, Name = "UpdatedTest1", Description = "UpdatedDescription1" };

            // Act
            var response = await client.PutAsJsonAsync($"/api/category/{updateCategory.Id}", updateCategory);

            // Assert
            response.IsSuccessStatusCode.Should().BeTrue(); // 確認狀態碼 200-299
            var updatedCategory = await response.Content.ReadFromJsonAsync<Category>();
            updatedCategory.Should().NotBeNull();
            updatedCategory.Name.Should().Be("UpdatedTest1");
            updatedCategory.Description.Should().Be("UpdatedDescription1");
        }

        [Fact]
        public async Task DeleteCategory_ReturnsNoContent()
        {
            // Arrange
            var client = _factory.CreateClient();
            var categoryId = 2;

            // Act
            var response = await client.DeleteAsync($"/api/category/{categoryId}");

            // Assert
            response.StatusCode.Should().Be(System.Net.HttpStatusCode.NoContent); // 確認狀態碼 204

            var getResponse = await client.GetAsync($"/api/category/{categoryId}");
            getResponse.StatusCode.Should().Be(System.Net.HttpStatusCode.NotFound); // 確認返回 404,表示找不到該類別
        }
    }
}

總結:這個做法是未來再做Side Project產品的時候,開始在規劃在程式可控的狀況下,其實可以直接實作整合測試專案,來進行提升自己產品品質。

 

元哥的筆記