JZuerlein/StockPriceTracker

GitHub: JZuerlein/StockPriceTracker

一个以高质量集成测试架构为核心的 ASP.NET Core minimal API 示例项目,展示了如何在同一套测试逻辑下对多数据库 provider 进行真实、可复用的自动化测试。

Stars: 2 | Forks: 1

Sponsored by
AuthorizationHub

# StockPriceTracker 一个小巧的 ASP.NET Core (.NET 10) minimal-API 演示项目,用于在身份验证下追踪股票价格。应用程序本身刻意保持精简,是作为演示“凭感觉编码 (vibe coded)”出来的——但**本仓库的重点在于其集成测试套件**。这些测试是经过深思熟虑编写的,旨在作为针对真实基础设施测试 minimal-API 应用程序的可重用模型。如果你只想学一件事,请阅读 [`StockPriceTracker.Tests.Integration`](StockPriceTracker.Tests.Integration)。

Watch on YouTube: Stop Inheriting Chaos — A Better Way to Write Integration Tests (Part 1)
📺 Stop Inheriting Chaos — A Better Way to Write Integration Tests (Part 1)

## 应用程序的功能 这是一个基于 ASP.NET Core Identity、支持 JWT 和 Cookie 身份验证的 minimal API: | Endpoint | Method | Auth | Description | | --- | --- | --- | --- | | `/auth/register` | POST | Anonymous | 注册一个 Identity 用户 | | `/auth/login` | POST | Anonymous | 登录,返回一个 JWT | | `/antiforgery/token` | GET | Anonymous | 颁发一个 antiforgery (CSRF) token | | `/stocks/{ticker}` | GET | Authenticated | 通过股票代码查询股票 | | `/stocks` | POST | `administrator` 角色 | 添加新股票 | 辅助组件包括:包含 **SQLite 和 PostgreSQL** 双 provider 的 EF Core、用于生成确定性时间戳的注入式 `TimeProvider`、通过 `TokenService` 签发的 JWT,以及在启动时进行的角色与管理员数据播种 (seeding)。 ### 项目布局 ``` StockPriceTracker/ The application Program.cs Composition root; exposes `partial class Program` for tests Endpoints/ Auth + Stock minimal-API endpoint groups Extensions/ServiceExtensions.cs AddSqlite / AddPostgreSql / AddIdentityAndAuth Data/ AppDbContext + startup DatabaseInitializer Services/TokenService.cs JWT creation StockPriceTracker.Tests.Integration/ The main event (see below) ``` ## 运行方式 需要 **.NET 10 SDK**。PostgreSQL 集成测试还需要运行中的 **Docker** 引擎(它们使用了 [Testcontainers](https://dotnet.testcontainers.org/))。 ``` # 运行应用程序(默认为 SQLite) dotnet run --project StockPriceTracker # 运行完整集成测试套件(SQLite + PostgreSQL) dotnet test ``` ## 集成测试——以及为什么它们值得借鉴 这些测试使用 `WebApplicationFactory` 在内存中启动**真实的应用程序**,并通过 HTTP 进行驱动。这里有几个模式非常值得引入到你自己的项目中。 ### 1. 一套测试逻辑,针对每个数据库 provider 运行 测试逻辑在一个泛型基类中只编写**一次**,然后通过为每个 provider 声明一个轻量级的具体子类,针对每个数据库 provider 执行: ``` public abstract class AddStockTestsBase : WebAppTestBase where TFixture : WebAppFixtureBase { [Fact] public async Task AddStock_WithJwtAuth_CreatesANewStock_WhenDataIsValid() { /* ... */ } } // Same tests, two real databases — zero duplicated test code: public class AddStockWithSqliteTests : AddStockTestsBase, IClassFixture { } public class AddStockWithPostgreSqlTests : AddStockTestsBase, IClassFixture { } ``` 通过相同的断言,你既能获得开发期间 **SQLite** 的速度,**又**能拥有真实 **PostgreSQL** 服务器的保真度。只需添加一个 fixture 和一个单行子类即可添加新的 provider。 ### 2. 共享 PostgreSQL 容器,每个 fixture 使用隔离的数据库 `PostgreSqlFixture` 为整个测试运行启动**一个** Testcontainers PostgreSQL 容器(只启动一次,延迟加载),然后在该容器内为每个 fixture 分配其自己唯一命名的数据库: ``` private static readonly PostgreSqlContainer SharedContainer = new PostgreSqlBuilder().Build(); private static readonly Lazy ContainerStart = new(() => SharedContainer.StartAsync()); private readonly string _databaseName = $"StockPriceTracker_{Guid.NewGuid():N}"; ``` 这是最佳平衡点:只需支付一次容器启动成本,同时保持测试类之间的数据彼此隔离。`SqliteFixture` 以相同的契约镜像了此过程,为每个 fixture 使用一个一次性的 `.sqlite` 文件,因此这两个 provider 是可以互换的。 ### 3. 流畅的已验证客户端构建器(支持 JWT *和* Cookie 认证) 测试永远不会去处理真实的密码或 token。一个测试认证 handler 被替换进了 DI 容器中,并在流畅构建器的背后根据你的需求发出所需的 claims: ``` var client = CreateClient() .WithJwtAuth(claims => claims.AsAdmin()) // or .WithCookieAuth(...) .Build(); ``` - `WithJwtAuth` / `WithCookieAuth` 选择认证方案,并将真实的 handler 替换为 `ConfigurableTestAuthHandler`。 - `ClaimsBuilder`(`AsAdmin()`, `AsUser(id)`, `WithRole(...)`, `WithClaim(...)`)使得测试中的身份标识明确且易于阅读。 - 由于该 handler 直接注入 claims,你可以无需建立登录流程即可测试**授权**(角色、策略)。 ### 4. 一流的 CSRF / antiforgery 测试 基于 Cookie 认证的测试会执行真实的 antiforgery pipeline。构建的客户端已启用 Cookie 处理,并且扩展方法让 CSRF 的配合操作只需一行代码——甚至包括*负面*测试用例: ``` await client.WithCsrfTokenAsync(); // fetch + attach the token like a browser would client.WithoutCsrfToken(); // prove protected calls are rejected without it ``` ### 5. 精心排序且自我检查的 fixture 生命周期 `WebAppFixtureBase.InitializeAsync` 将其启动过程明确划分为四个有序、命名的阶段,然后**断言其自身的后置条件**,这样未来如果某次重构意外导致 host 的构建变成了延迟加载,它会在 setup 阶段就响亮地报错,而不是在后续神秘地失败: ``` await StartDatabaseAsync(); // 1. DB is up … BuildFactory(); // 2. … before the host reads its connection string var host = MaterializeHost(); // 3. force the host to build now, deterministically await SeedAsync(host); // 4. seed known data ``` 基类中添加了大量关于每一步背后*原因*的注释——它是为了让人阅读而设计的。它还集中了每个测试所需的辅助方法:用于对数据库进行断言的 `ExecuteDbContextAsync(...)`、用于深入 DI 的 `GetService()` / `CreateScope()`,以及一个包含已知 fixture 的已播种 `Stocks` 数组。 ### 6. 确定性时间与生成的测试数据 `TimeProvider` 被注入并在测试 host 中被替换,因此时间戳是可控的,而不是依赖于系统墙上时钟。请求的 payload 是使用 [AutoFixture](https://github.com/AutoFixture/AutoFixture) 生成的,这使得测试可以专注于行为本身,而不是手写的样本数据。 ### 整合起来 一个完整的测试从上到下读起来就是:*准备身份标识 → 通过 HTTP 执行操作 → 断言结果*,所有的底层基础设施都隐藏在了基类背后: ``` var request = CreateRequest(); var client = CreateClient().WithJwtAuth(claims => claims.AsAdmin()).Build(); var response = await client.PostAsJsonAsync("/stocks", request); Assert.Equal(HttpStatusCode.Created, response.StatusCode); var created = await response.Content.ReadFromJsonAsync(); Assert.Equal(request.Ticker, created!.Ticker); ``` 刚才那个测试就已经针对真实的 PostgreSQL 和 SQLite 运行过了。 ## 许可证 基于 [MIT License](LICENSE) 发布——请随意将这些测试模式复制到你自己的项目中。

AuthorizationHub for ASP.NET Core — keep your logins, fix your permissions

## 专为 ASP.NET Core 构建的授权管理插件 **将组织架构树转化为 Claims。** 大多数授权规则都与组、职位角色和人员有关。这已经实现过很多次了,为什么还要重新造轮子呢? [AuthorizationHub](https://authorizationhub.com) 的强大之处在于,当用户的请求在 ASP.NET Core pipeline 中被处理时,与用户相关联的租户、组和角色都会转化为身份 claims。这些 claims 是特定于你的应用程序的,并且可以在授权策略中使用。这与 ASP.NET Core 的安全模型完美契合,它意味着你可以通过更改用户的角色和组成员身份,来改变谁可以在你的应用程序中执行操作。无需修改代码并重新部署 Web 应用程序。
标签:ASP.NET Core, Syscall, Web开发, 单元测试, 测试用例, 测试自动化, 请求拦截, 集成测试