2017-04-06 120 views
1

我有在控制器下面的方法(當用戶將機器人說歡迎消息)來發送從控制器本身的消息Conversations.SendToConversationAsync崩潰上單元測試

 private static async Task<string> OnSendOneToOneMessage(Activity activity, 
     IList<Attachment> attachments = null) 
    { 
     var reply = activity.CreateReply(); 
     if (attachments != null) 
     { 
      reply.Attachments = attachments; 
     } 

     if (_connectorClient == null) 
     { 
      _connectorClient = new ConnectorClient(new Uri(activity.ServiceUrl)); 
     } 

     var resourceResponse = await _connectorClient.Conversations.SendToConversationAsync(reply); 
     return resourceResponse.Id; 
    } 

和單元測試長相像這樣

[TestClass] 
public sealed class MessagesControllerTest 
{ 
    [Test] 
    public async Task CheckOnContactRelationUpdate() 
    { 
     // Few more setup related to dB <deleted> 
     var activity = new Mock<Activity>(MockBehavior.Loose); 
     activity.Object.Id = activityMessageId; 
     activity.Object.Type = ActivityTypes.ContactRelationUpdate; 
     activity.Object.Action = ContactRelationUpdateActionTypes.Add; 
     activity.Object.From = new ChannelAccount(userId, userName); 
     activity.Object.Recipient = new ChannelAccount(AppConstants.BotId, AppConstants.BotName); 
     activity.Object.ServiceUrl = serviceUrl; 
     activity.Object.ChannelId = channelId; 
     activity.Object.Conversation = new ConversationAccount {Id = Guid.NewGuid().ToString()}; 
     activity.Object.Attachments = Array.Empty<Attachment>(); 
     activity.Object.Entities = Array.Empty<Entity>(); 

     var messagesController = 
      new MessagesController(mongoDatabase.Object, null) 
      { 
       Request = new HttpRequestMessage(), 
       Configuration = new HttpConfiguration() 
      }; 

     // Act 
     var response = await messagesController.Post(activity.Object); 
     var responseMessage = await response.Content.ReadAsStringAsync(); 

     // Assert 
     Assert.IsNotEmpty(responseMessage); 
    } 
} 

當用戶添加bor時,OnSendOneToOneMessage方法工作正常。但它爲單元測試崩潰。似乎我錯過了POST的一些設置?

堆棧跟蹤爲

Result StackTrace: 
    at System.Net.Http.StringContent.GetContentByteArray(String content, Encoding encoding) 
    at System.Net.Http.StringContent..ctor(String content, Encoding encoding, String mediaType) 
    at System.Net.Http.StringContent..ctor(String content) 
    at <>.Controllers.MessagesController.<Post>d__4.MoveNext() in 
    C:\Users....MessagesController.cs:line 75 

---在System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(任務task) 從先前的位置堆棧跟蹤,其中引發異常--- 結束在系統.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(任務的任務) 在System.Runtime.CompilerServices.TaskAwaiter`1.GetResult() 在BotTest.Controllers.MessagesControllerTest.d__0.MoveNext()在 C:\用戶.... MessagesControllerTest.cs:第75行 ---堆棧結束在NUnit.Framework.Internal.AsyncInvocationRegion.AsyncTaskInvocationRegion.WaitFor PendingOperationsToComplete(對象invocationResult) 在NUnit.Framework.Internal.Commands.TestMethodCommand.RunAsyncTestMethod(TestExecutionContext上下文) 結果消息從先前的位置,其中引發異常--- CE: System.ArgumentNullException:值不能爲空。 參數名:內容

這裏是輸出

Exception thrown: 'System.ArgumentNullException' in mscorlib.dll 
Exception thrown:  'Microsoft.Rest.TransientFaultHandling.HttpRequestWithStatusException' in Microsoft.Rest.ClientRuntime.dll 
Exception thrown: 'Microsoft.Rest.TransientFaultHandling.HttpRequestWithStatusException' in mscorlib.dll 
Exception thrown: 'Microsoft.Rest.TransientFaultHandling.HttpRequestWithStatusException' in Microsoft.Rest.ClientRuntime.dll 
Exception thrown: 'Microsoft.Rest.TransientFaultHandling.HttpRequestWithStatusException' in mscorlib.dll 
Exception thrown: 'System.Net.Http.HttpRequestException' in System.Net.Http.dll 
Exception thrown: 'System.UnauthorizedAccessException' in Microsoft.Bot.Connector.dll 
Exception thrown: 'System.UnauthorizedAccessException' in mscorlib.dll 
Exception thrown: 'System.UnauthorizedAccessException' in System.Net.Http.dll 
Exception thrown: 'System.UnauthorizedAccessException' in mscorlib.dll 
Exception thrown: 'System.UnauthorizedAccessException' in mscorlib.dll 

注:我嘗試通過該憑證在所有不同的方式。它仍然在單元測試中崩潰。

+0

但你在哪裏嘲諷連接器客戶端? –

+0

我最初嘗試嘲笑,但這一個我正在嘗試從集成測試更多。因此,每次在OnSendOneToOneMessage方法中運行測試時都會創建連接器客戶端。 – dolbyarun

+0

你使用的是什麼服務網址?您是否嘗試過使用[this構造函數](https://github.com/Microsoft/BotBuilder/blob/master/CSharp/Library/Microsoft.Bot.Connector.Shared/ConnectorAPI/ConnectorClient.cs#L207)對證書進行硬編碼?無論如何,我並不認爲這是進行集成測試的正確方法 –

回答

2

根據您的意見,看起來您想要做的是功能/集成測試。

爲此,我建議使用Direct Line。唯一需要注意的是該機器人需要託管,但它確實非常強大。該方法包括使用Direct Line向託管機器人發送消息,捕獲響應並根據這些Bot測試用例進行斷言。

查看所有實現的最佳方法是檢查AzureBot tests project。這種方法有很多功能測試。

美妙之處在於測試是非常簡單的,他們只是定義情景:

public async Task ShoudListVms() 
{ 
    var testCase = new BotTestCase() 
    { 
     Action = "list vms", 
     ExpectedReply = "Available VMs are", 
    }; 

    await TestRunner.RunTestCase(testCase); 
} 

所有魔法發生在TestRunner。類別BotHelper與Direct Line有所有交互,它在General類中進行了配置和初始化。

我知道這很重要,你需要改變一些東西,但是我認爲如果你花時間去掌握它,它確實會幫助你做一流的功能測試。

+0

解決這個問題的方法如下。 – dolbyarun

+0

哪條路?你使用了答案中解釋的方法嗎?如果是這樣,你可以將問題標記爲已回答。 –

+0

將您的答覆標記爲答案。肯定是的。考慮我作爲一種方法的方式。 – dolbyarun

0

以下面的方式解決了這個問題。

首先,問題在哪裏? :問題是在端點(ApiController)調用SendToConversationAsync失敗並出現身份驗證錯誤。無論你使用BotBuilder中提供的「MockConnectorFactory」類來模擬連接器,還是創建一個新的ConnectorClient,如果URI是白色列出的(在我的情況下,它是azurewebsite,所以它是白色列出的),auth令牌將不會被生成。這是我們在最終通話中遇到認證錯誤的地方。傳遞證書不會有太大的幫助,因爲令牌只是爲非白名單URI生成的。

解決方案:派生一個TestConnectorClient和實現自己的IConversations。 在您自己的IConversation實現中,設置憑證以獲取有效的不記名令牌。

的TestConnectorClient看起來像這樣

internal sealed class TestConnectorClient : ConnectorClient 
{ 
    public TestConnectorClient(Uri uri) : base(uri) 
    { 
     MockedConversations = new TestConversations(this); 
    } 

    public override IConversations Conversations => MockedConversations; 

    public IConversations MockedConversations { private get; set; } 
} 

下面

public sealed class TestConversations : IConversations 
{ 
    public TestConversations(ConnectorClient client) 
    { 
     Client = client; 
    } 

    private ConnectorClient Client { get; } 

    public Task<HttpOperationResponse<object>> CreateConversationWithHttpMessagesAsync(
     ConversationParameters parameters, Dictionary<string, List<string>> customHeaders = null, 
     CancellationToken cancellationToken = new CancellationToken()) 
    { 
     return null; 
    } 

    public async Task<HttpOperationResponse<object>> SendToConversationWithHttpMessagesAsync(Activity activity, 
     string conversationId, Dictionary<string, List<string>> customHeaders = null, 
     CancellationToken cancellationToken = default(CancellationToken)) 
    { 
     if (activity == null) 
     { 
      throw new ValidationException(ValidationRules.CannotBeNull, "activity"); 
     } 
     if (conversationId == null) 
     { 
      throw new ValidationException(ValidationRules.CannotBeNull, "conversationId"); 
     } 

     // Construct URL 
     var baseUrl = Client.BaseUri.AbsoluteUri; 
     var url = new Uri(new Uri(baseUrl + (baseUrl.EndsWith("/") ? "" : "/")), 
      "v3/conversations/{conversationId}/activities").ToString(); 
     url = url.Replace("{conversationId}", Uri.EscapeDataString(conversationId)); 
     // Create HTTP transport objects 
     var httpRequest = new HttpRequestMessage 
     { 
      Method = new HttpMethod("POST"), 
      RequestUri = new Uri(url) 
     }; 

     var cred = new MicrosoftAppCredentials("{Your bot id}", "{Your bot pwd}"); 
     var token = await cred.GetTokenAsync(); 
     httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); 
     // Set Headers 
     if (customHeaders != null) 
     { 
      foreach (var header in customHeaders) 
      { 
       if (httpRequest.Headers.Contains(header.Key)) 
       { 
        httpRequest.Headers.Remove(header.Key); 
       } 
       httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value); 
      } 
     } 

     // Serialize Request 
     var requestContent = SafeJsonConvert.SerializeObject(activity, Client.SerializationSettings); 
     httpRequest.Content = new StringContent(requestContent, Encoding.UTF8); 
     httpRequest.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8"); 
     // Set Credentials 
     if (Client.Credentials != null) 
     { 
      cancellationToken.ThrowIfCancellationRequested(); 
      await Client.Credentials.ProcessHttpRequestAsync(httpRequest, cancellationToken).ConfigureAwait(false); 
     } 
     // Send Request 
     cancellationToken.ThrowIfCancellationRequested(); 
     var httpResponse = await Client.HttpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false); 
     var statusCode = httpResponse.StatusCode; 
     cancellationToken.ThrowIfCancellationRequested(); 
     string responseContent; 
     if ((int) statusCode != 200 && (int) statusCode != 201 && (int) statusCode != 202 && 
      (int) statusCode != 400 && (int) statusCode != 401 && (int) statusCode != 403 && 
      (int) statusCode != 404 && (int) statusCode != 500 && (int) statusCode != 503) 
     { 
      var ex = new HttpOperationException(
       $"Operation returned an invalid status code '{statusCode}'"); 
      responseContent = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false); 
      ex.Request = new HttpRequestMessageWrapper(httpRequest, requestContent); 
      ex.Response = new HttpResponseMessageWrapper(httpResponse, responseContent); 
      httpRequest.Dispose(); 
      httpResponse.Dispose(); 
      throw ex; 
     } 
     // Create Result 
     var result = new HttpOperationResponse<object> 
     { 
      Request = httpRequest, 
      Response = httpResponse 
     }; 

     responseContent = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false); 
     try 
     { 
      result.Body = 
       SafeJsonConvert.DeserializeObject<ResourceResponse>(responseContent, 
        Client.DeserializationSettings); 
     } 
     catch (JsonException ex) 
     { 
      httpRequest.Dispose(); 
      httpResponse.Dispose(); 
      throw new SerializationException("Unable to deserialize the response.", responseContent, ex); 
     } 
     return result; 
    } 

    public Task<HttpOperationResponse<object>> UpdateActivityWithHttpMessagesAsync(string conversationId, 
     string activityId, Activity activity, 
     Dictionary<string, List<string>> customHeaders = null, 
     CancellationToken cancellationToken = new CancellationToken()) 
    { 
     return null; 
    } 

    public Task<HttpOperationResponse<object>> ReplyToActivityWithHttpMessagesAsync(string conversationId, 
     string activityId, Activity activity, 
     Dictionary<string, List<string>> customHeaders = null, 
     CancellationToken cancellationToken = new CancellationToken()) 
    { 
     return null; 
    } 

    public Task<HttpOperationResponse<ErrorResponse>> DeleteActivityWithHttpMessagesAsync(string conversationId, 
     string activityId, Dictionary<string, List<string>> customHeaders = null, 
     CancellationToken cancellationToken = new CancellationToken()) 
    { 
     return null; 
    } 

    public Task<HttpOperationResponse<object>> GetConversationMembersWithHttpMessagesAsync(string conversationId, 
     Dictionary<string, List<string>> customHeaders = null, 
     CancellationToken cancellationToken = new CancellationToken()) 
    { 
     return null; 
    } 

    public Task<HttpOperationResponse<object>> GetActivityMembersWithHttpMessagesAsync(string conversationId, 
     string activityId, Dictionary<string, List<string>> customHeaders = null, 
     CancellationToken cancellationToken = new CancellationToken()) 
    { 
     return null; 
    } 

    public Task<HttpOperationResponse<object>> UploadAttachmentWithHttpMessagesAsync(string conversationId, 
     AttachmentData attachmentUpload, 
     Dictionary<string, List<string>> customHeaders = null, 
     CancellationToken cancellationToken = new CancellationToken()) 
    { 
     return null; 
    } 
} 

注意的Testconversation實現:如果目標是單元測試,方法SendToConversationWithHttpMessagesAsync可以簡單地返回相應的預期響應。無需做出真正的呼叫。在這種情況下,對於功能測試,我正在進行真正的調用

和測試用例用於檢查ContactRelationUpdateActionTypes

[Test] 
    [TestCase(ContactRelationUpdateActionTypes.Add, true)] 
    [TestCase(ContactRelationUpdateActionTypes.Add, false)] 
    [TestCase(ContactRelationUpdateActionTypes.Remove, false)] 
    public async Task CheckOnContactRelationUpdate(string actionType, bool isBrandNewUser) 
    { 
     // Mock dB here 

     var activityMessageId = Guid.NewGuid().ToString(); 
     const string userName = "{Some name}"; 
     const string userId = "{A real user id for your bot}"; 
     const string serviceUrl = "https://smba.trafficmanager.net/apis/"; 
     const string channelId = "skype"; 
     var activity = new Activity 
     { 
      Id = activityMessageId, 
      Type = ActivityTypes.ContactRelationUpdate, 
      Action = ContactRelationUpdateActionTypes.Add, 
      From = new ChannelAccount(userId, userName), 
      Recipient = new ChannelAccount(AppConstants.BotId, AppConstants.BotName), 
      ServiceUrl = serviceUrl, 
      ChannelId = channelId, 
      Conversation = new ConversationAccount {Id = userId}, 
      Attachments = Array.Empty<Attachment>(), 
      Entities = Array.Empty<Entity>() 
     }; 

     var connectorClient = new TestConnectorClient(new Uri(activity.ServiceUrl)); 
     connectorClient.MockedConversations = new TestConversations(connectorClient); 

     var messagesController = new MessagesController(mongoDatabase.Object, connectorClient) 
     { 
      Configuration = new HttpConfiguration(), 
      Request = new HttpRequestMessage() 
     }; 

     // Act 
     var response = await messagesController.Post(activity); 
     var responseMessage = await response.Content.ReadAsStringAsync(); 

     // Assert 
     switch (actionType) 
     { 
      case ContactRelationUpdateActionTypes.Add: 
       Assert.IsNotEmpty(responseMessage); 
       break; 
      case ContactRelationUpdateActionTypes.Remove: 
       Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); 
       break; 
     } 
    } 

在上述試驗中,我測試3個場景。

  1. 當機器人獲得由新用戶增加=>預期的結果是,使用SendToConversationAsync,它返回一個ResourceResponse和我ApiController回來後的資源ID爲HttpResponseMessage內容將被髮送歡迎信息。
  2. 當機器人被重新由用戶添加=>預期的結果是,一個迎回的消息是使用SendToConversationAsync,它返回一個ResourceResponse和我ApiController回傳的資源ID爲HttpResponseMessage內容發佈。
  3. 當機器人由用戶除去被=>預期的結果是,在我的情況,在用戶模型具有根據機器人是否被添加或從聯繫人中刪除被設置/復位的場IsFriend。雖然我可以檢查刪除的特定字符串,但我只是單單檢查確定的響應。