Open Jeff-Tian opened 8 months ago
另外,在 content 中带有 CREATE DATABASE 或者 DROP DATABASE 的字眼,也会触发 501 。
curl --location 'https://api.weixin.qq.com/cgi-bin/draft/add?access_token=Rf3ffELHjup3k9gzj1HHzMUutBtmAgubJtyXQ0HxATbf-T9SqrWyXICBNeAHATSE' \
--header 'Content-Type: application/json' \
--data '{
"articles": [
{
"title": "test",
"author": "哈德韦",
"content": "test<p>docker 真是好,docker compose 更是不错。然而,在频繁的启动关闭中,有时候也很容易让人烦,比如很多测试需要启动一些像数据库之类的依赖,就不得不在 README 里提一句,跑测试前需要先 <code>docker compose up -d</code> 一下。虽然似乎很简单,但仍然有点麻烦,不免会让人在此栽跟着,连大佬也不能幸免,比如:</p>\n<p><img src=\"http://mmbiz.qpic.cn/mmbiz_png/AhHNmKXia2gnATu1yIWGFvInk2er3ooribW7jhk0ymllic39IzhfXKiaXT8tJwcIiaVyA8wmQI9RYy3I2j4orl9EHMw/0?wx_fmt=png\" alt=\"\"></p><p>我发现通过 test containers 可以不用再手动去执行这些命令。</p><p>感觉上,test containers 就是一个全托管的 docker compose,即只需要写测试代码就好了,至于启动容器和销毁容器,不用再手动做了。当然,底层还是容器,可以在发起执行测试的命令之后,通过 docker desktop 看到测试容器正在启动中:</p><p><img src=\"http://mmbiz.qpic.cn/mmbiz_png/AhHNmKXia2gnATu1yIWGFvInk2er3ooribxp7ic9XB8OBTGl8jUfJ04cOauaCcn8EukSic7RJPicNB1ZqHlxuZbewqQ/0?wx_fmt=png\" alt=\"\"></p><p>在测试运行结束后,可以看到容器被自动销毁:</p><p><img src=\"http://mmbiz.qpic.cn/mmbiz_png/AhHNmKXia2gnATu1yIWGFvInk2er3ooribeheElM4xplsDQ9wsUfTH0ATh8IZsalMWKkeyfrLgg4n4YIeJOUIRIg/0?wx_fmt=png\" alt=\"\"></p><p>这里分享一个完整的测试案例:应用程序启动了一个 pulsar 消费者,用来消费 pulsar 上某一主题上的事件消息,从而用来更新用户最近和系统互动的时间。要测试这种场景,就可以让测试跑起来之前,通过 test containers 运行一个 pulsar 服务,然后就可以在测试代码中使用真正的 pulsar 消费者,避免写一堆模拟客户端和消费者等等。</p><p>以 dotnet core 应用举例,首先需要写一个 TestingBase 类,做一些测试生命周期管理,并将使用 test containers 启动 pulsar 服务的代码放在其中:</p><pre><code class=\"language-java\">public abstract class TestingFixtureBase(string databaseName){ private readonly PulsarContainer _pulsar = new PulsarBuilder().WithImage("apachepulsar/pulsar:2.10.0").Build(); protected abstract IServiceProvider Services { get; } protected IHostBuilder HostBuilder { get; private set; } protected async Task Initialize() { await _pulsar.StartAsync(); HostBuilder = Program.CreateHostBuilder([]); HostBuilder.UseEnvironment("local"); HostBuilder.UseSerilog(); ConfigureConfiguration(HostBuilder); HostBuilder.ConfigureServices((hostContext, services) => { var dbConfig = hostContext.Configuration.Get<DatabaseConfiguration>()!; RecreateTestDatabase(dbConfig.Host, dbConfig.Username, dbConfig.Password, dbConfig.DatabaseName); }); } private void RecreateTestDatabase(string host, string username, string password, string databaseName) { // 创建连接字符串时不要指定数据库名称,否则如果数据库不存在将抛出异常。 var connectionString = new MySqlConnectionStringBuilder { Server = host, UserID = username, Password = password, Port = 3306, // 在 GitHub Actions 中运行时需要 SslMode = MySqlSslMode.None, }; using var connection = new MySqlConnection(connectionString.ConnectionString); connection.Open(); using var dropCommand = new MySqlCommand($"DROP DATABASE IF EXISTS `{databaseName}`", connection); dropCommand.ExecuteNonQuery(); using var createCommand = new MySqlCommand($"CREATE DATABASE `{databaseName}`", connection); createCommand.ExecuteNonQuery(); } private void ConfigureConfiguration(IHostBuilder hostBuilder) { hostBuilder.ConfigureAppConfiguration((_, config) => { var databaseHost = Environment.GetEnvironmentVariable("DATABASE_HOST") ?? "localhost"; config.AddInMemoryCollection(new Dictionary<string, string?> { ["DATABASE_HOST"] = databaseHost, ["DATABASE_PORT"] = "3306", ["DATABASE_USERNAME"] = "root", ["DATABASE_PASSWORD"] = "localdb123", ["DATABASE_NAME"] = databaseName, ["DATABASE_SSL_MODE"] = nameof(MySqlSslMode.None), ["DATABASE_IAM_AUTHENTICATION_ENABLED"] = "false", ["DATABASE_CONNECTION_IDLE_TIMEOUT"] = "3600", ["DATABASE_CONNECTION_LIFETIME"] = "3600", ["PULSAR_CLIENT_ID"] = "fake-client-id", ["PULSAR_CLIENT_SECRET"] = "fake-client-secret", ["PULSAR_SERVICE_URL"] = _pulsar.GetBrokerAddress(), ["TOPIC"] = "persistent://public/default/your-topic-name", ["SUBSCRIPTION_NAME"] = "your-sub-name", }); }); } protected async Task Dispose() { var dbContext = Services.CreateScope().ServiceProvider.GetRequiredService<ApplicationDbContext>(); await dbContext.Database.EnsureDeletedAsync(); await _pulsar.DisposeAsync().AsTask(); }}</code></pre><p>然后需要创建一个类继承自上面的 TestingBase,并实现 IAsyncLifetime 类,写一些测试通用的代码,用来做测试夹具:</p><pre><code class=\"language-csharp\">public class BackgroundJobFixture() : TestingFixtureBase("background_jobs_tests"), IAsyncLifetime{ private IServiceProvider _serviceProvider; protected override IServiceProvider Services => _serviceProvider; public IHost Host; public async Task InitializeAsync() { await Initialize(); Host = HostBuilder.Build(); _serviceProvider = Host.Services; var migrator = _serviceProvider.GetRequiredService<DatabaseMigrator>(); migrator.ExecuteMigrations(true); } public Task DisposeAsync() => Dispose(); [CollectionDefinition("background_job_scenarios", DisableParallelization = false)] public class BackgroundJobsScenarioCollection : ICollectionFixture<BackgroundJobFixture>; [Collection("background_job_scenarios")] public abstract class BackgroundJobsScenarioContext : IAsyncLifetime { private readonly IServiceScope _serviceScope; private readonly IServiceProvider _serviceProvider; protected BackgroundJobsScenarioContext(BackgroundJobFixture fixture) { _serviceScope = fixture.Services.CreateScope(); _serviceProvider = _serviceScope.ServiceProvider; } protected T GetService<T>() where T : class => _serviceProvider.GetRequiredService<T>(); public Task InitializeAsync() { return Task.CompletedTask; } public Task DisposeAsync() { _serviceScope.Dispose(); return Task.CompletedTask; } }}</code></pre><p>最后,测试可能像是长这样:</p><pre><code class=\"language-csharp\">public class EventListenerJobEnd2EndTests : BackgroundJobFixture.BackgroundJobsScenarioContext, IAsyncLifetime{ private readonly ApplicationDbContext _applicationDbContext; private readonly IPulsarClient _client; private readonly IProducer<string> _producer; private readonly IHost _host; public EventListenerJobEnd2EndTests(BackgroundJobFixture fixture) : base(fixture) { _host = fixture.Host; _applicationDbContext = GetService<ApplicationDbContext>(); var configuration = GetService<IConfiguration>(); _client = PulsarClient.Builder() .ServiceUrl(new Uri(configuration["PULSAR_SERVICE_URL"]!)) .Build(); _producer = _client.NewProducer(Schema.String) .Topic(configuration["TOPIC"]!) .Create(); } public new async Task DisposeAsync() { await _producer.DisposeAsync(); await _client.DisposeAsync(); await base.DisposeAsync(); } [Fact] public async Task Should_ProcessEvent_WhenEventComes() { // arrange var user = new UserBuilder().CreateValid().User; _applicationDbContext.Users.Add(user); await _applicationDbContext.SaveChangesAsync(); await _host.StartAsync(); var event = (V1EnvelopeCloudEventBuilder.Create().WithPublicUserId(user.PublicId).Generate()); // act _ = await _producer.Send(JsonSerializer.Serialize(event)).ConfigureAwait(true); // 等待事件被处理。 await Task.Delay(2000); // assert var actual = _applicationDbContext.ApplicationUserActivities.FirstOrDefault(x => x.UserId == user.Id); actual.Should().NotBeNull(); actual!.LastActivity.Should().BeCloseTo(loyaltyEvent.Data.Timestamp, TimeSpan.FromSeconds(1)); }",
"content_source_url": "https://jefftian.dev",
"need_open_comment": 1,
"thumb_media_id": "PiIEZ-GBv-fmg8_8wVQ_0b8gPhjXvzDXGRdvwIHm65K4zEVfmS-Gf2FS3Tj9_KfW"
}
]
}'
以上请求中,将 content 的 CREATE 和 DROP DATABASE 改掉即可成功。