每一個Teams bot實際上就是一個web api服務,這個服務通過Bot Framework和Teams進行通訊,所以對于Teams app的測試就是對于一個api service的測試。
軟件行業發展到如今,測試技術已經趨于成熟。單元測試,冒煙測試,整合測試。。。等等。那什么是Service level的測試。這里所謂的服務級的測試類似于Integration Test,就是指把整個api服務看成是一個黑盒,對這個服務的各個api接口作為最小單位,進行測試。與Integration Test不同之處在于,Service level測試更加側重于服務本身,可以盡量mock掉服務的外部依賴項。
Service Level的測試在如今微服務的時代特別實用,如果使用大量的單元測試,把每個class的每個方法都層層保護,一旦將來改動了代碼,對測試代碼的更新也是一個較大的工作量,也就是說代碼被測試限制的特別死。相反,微服務的時代因為每個服務都不會非常大,我們需要給代碼一些改動的空間,我們關心的是每個api接口對于傳入的輸入,是否可以產生正確的輸出。
而且,我在使用ServiceLevel測試對我的抽獎機器人進行測試的時候,能夠很好的發現很多dead code,就是一些永遠也不會被執行到的死代碼。這些代碼應該會刪掉,保持代碼的簡潔。
那如何做呢?ASP.NET Core早就為我們準備好了ServiceLevel測試的利器:TestServer。微軟官方文檔里也有很多介紹如何使用TestServer來做整合測試,我們來看一個最簡單的例子:
public class TestServerFixture : IDisposable
{private readonly TestServer _testServer;public HttpClient Client { get; }public TestServerFixture(){var builder = new WebHostBuilder().UseEnvironment("Development").UseStartup<Startup>();_testServer = new TestServer(builder);Client = _testServer.CreateClient();}public void Dispose(){Client.Dispose();_testServer.Dispose();}
}[Fact]
public async Task WhenGetMethodIsInvokedWithoutAValidToken_GetShouldAnswerUnAuthorized()
{using (TestServerFixture fixture = new TestServerFixture()){// Actvar response = await fixture.Client.GetAsync("/api/values/5");// Assertresponse.StatusCode.Should().Be(HttpStatusCode.Unauthorized);}
}
當然,由于LuckyDraw bot里使用到了很多Azure table storage服務,我們在測試中,不應該使用真實的azure storage,不然多個測試用例并發執行的時候,數據肯定就亂掉了,而且會相互沖突,導致測試結果無法預料。所以在測試的時候我們需要把api服務的外部依賴項都mock掉,比如我在LuckyDraw bot里就mock了Bot connector,因為在測試中我們不能,也不應該真實的往teams里發送東西。
說了這么多,還是上代碼,讓大家對這個有一個更加直觀的認識:
[Fact]
public async Task WhenEverythingIsGood_SendTextHelp_ReplyHelpMessage()
{using (var server = CreateServerFixture(ServerFixtureConfigurations.Default))using (var client = server.CreateClient()){var response = await client.SendTeamsText("<at>bot name</at>help");response.StatusCode.Should().Be(HttpStatusCode.OK);var createdMessages = server.Assert().GetCreatedMessages();createdMessages.Should().HaveCount(1);createdMessages[0].Activity.Text.Should().StartWith("Hi there, To start a lucky draw");}
}public static async Task<HttpResponseMessage> SendTeamsText(this HttpClient httpClient,string text,string locale = null,double? offsetHours = null)
{var activity = new Activity{ServiceUrl = "https://service-url.com",ChannelId = "msteams",Type = ActivityTypes.Message,Text = text,Locale = locale ?? "en-us",LocalTimestamp = offsetHours.HasValue ? new DateTimeOffset(2018, 1, 1, 1, 1, 1, 1, TimeSpan.FromHours(offsetHours.Value)) : (DateTimeOffset?)null,From = new ChannelAccount("id", "name"),Recipient = new ChannelAccount("bot id", "bot name"),Conversation = new ConversationAccount(isGroup: true, id: "conv id", name: "conv name"),ChannelData = new TeamsChannelData{Tenant = new TenantInfo { Id = Guid.NewGuid().ToString() },Team = new TeamInfo { Id = Guid.NewGuid().ToString() },Channel = new ChannelInfo { Id = Guid.NewGuid().ToString() },}};return await httpClient.SendActivity(activity);
}
可以看到我們模擬了Teams的Activity,把我們自己生成的一個activity傳遞給了我們api接口,然后check了api發送給Teams的消息是不是我們想要的內容。