整合測試
上述自己需求要做Side Project要做一些整合測試,SQL Lite向來是一個省錢方案的做法。
需安裝套件
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.SqlLite
- 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
安裝所需套件
- Microsoft.AspNetCore.Mvc.Testing
- Microsoft.EntityFrameworkCore.InMemory
- 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產品的時候,開始在規劃在程式可控的狀況下,其實可以直接實作整合測試專案,來進行提升自己產品品質。
元哥的筆記