2017-04-06 3 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); 
    } 
} 

メソッドOnSendOneToOneMessageは、ユーザーがborを追加したときに正常に動作します。しかし、単体テストではクラッシュします。私は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(タスクタスク) で例外がスローされた以前の位置からスタックトレース--- の端部であります CでBotTest.Controllers.MessagesControllerTest.d__0.MoveNextでSystem.Runtime.CompilerServices.TaskAwaiter`1.GetResultで.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(タスクタスク) () ():\ユーザー.... MessagesControllerTest.cs:line 75 ---スタックの終了tra NUnit.Framework.Internal.AsyncInvocationRegion.AsyncTaskInvocationRegion.WaitFor PendingOperationsToComplete(オブジェクトinvocationResult)NUnit.Framework.Internal.Commands.TestMethodCommand.RunAsyncTestMethodで (TestExecutionContextコンテキスト) 結果メッセージで例外がスローされた以前の位置--- からCE: System.ArgumentNullException:値にnullを設定することはできません。 パラメータ名:コンテンツ

そして、ここでは、出力

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 

NOTEです:私はすべての異なる方法で証明書を渡してみました。それでもユニットテストでクラッシュします。

+0

ただし、コネクタクライアントを嘲笑していますか? –

+0

私は最初に嘲笑を試みましたが、この1つは統合テストからさらに試しています。したがって、OnSendOneToOneMessageメソッドでテストが実行されるたびに、コネクタクライアントが作成されます。 – dolbyarun

+0

あなたはどのサービスURLを使用していますか? [このコンストラクタ](https://github.com/Microsoft/BotBuilder/blob/master/CSharp/Library/Microsoft.Bot.Connector.Shared/ConnectorAPI/ConnectorClient.cs#L207)の資格情報をハードコーディングしてみましたか?いずれにせよ、私はこれを統合テストの正しい方法とは思わない –

答えて

2

あなたのコメントに基づいて、あなたがしたいのは機能/統合テストです。

このため、私はDirect Lineを使用することをおすすめします。唯一の注意点は、ボットをホストする必要があることですが、本当に強力です。アプローチは、Direct Lineを使用してホステッドボットにメッセージを送信し、レスポンスをキャプチャし、それらのボットテストケースに基づいてアサートを実行します。

これを実現する最良の方法は、AzureBot tests projectをチェックすることです。このアプローチに続いて数多くの機能テストが行​​われています。

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

    await TestRunner.RunTestCase(testCase); 
} 

すべての魔法がTestRunnerで起こる:

美しさは、テストは非常に簡単であることを、彼らはただのシナリオを定義しています。 BotHelperクラスは、Generalクラスで構成および初期化されたDirect Lineとのすべてのやり取りを行います。

私はこれが大いに消化されていることを知っています。そして、あなたはここやそこを変える必要がありますが、これをマスターする時間を取れば、ファーストクラスの機能テストを行うのに本当に役立つでしょう。

+0

これを次のように解決しました。 – dolbyarun

+0

どのようにですか?答えに説明されているアプローチを使用しましたか?そうであれば、その質問に回答としてマークすることができます。 –

+0

回答を回答として記入してください。間違いなくそれです。もう1つのアプローチとして私がやったやり方を考えてみましょう。 – dolbyarun

0

これを次のように解決しました。

まず、問題はどこですか? :エンドポイント(ApiController)に問題があり、認証エラーでSendToConversationAsyncが失敗します。 BotBuilderで利用可能な "MockConnectorFactory"クラスを使用してコネクターを偽装するか、新しいConnectorClientを作成するかにかかわらず、URIがホワイトリスト(私の場合はazurewebsiteだったので白リストになっています)であれば、authトークンは生成されません。これは、最終的な呼び出しで認証エラーが発生した場所です。また、資格情報を渡すことは、トークンが非ホワイトリストURIに対してのみ生成されるため、あまり役に立ちません。

解決策:TestConnectorClientを派生させ、独自のIConversationsを実装します。 独自のIConversation実装内で、有効なベアラトークンを取得するようにクレデンシャルを設定します。

はTestConnectorClientは

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; 
    } 
} 

NOTE以下この

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; } 
} 

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. =>期待される結果は、ウェルカムメッセージがResourceResponseと私のApiControllerはHttpResponseMessageのコンテンツとしてリソースIDをポストバック返すSendToConversationAsyncを、使用して掲載されています。
  2. ボットがユーザーによって追加された場合=>期待される結果は、SendToConversationAsyncを使ってウェルカムバックメッセージが送信され、ResourceResponseを返し、ApiControllerがリソースIDをHttpResponseMessageのコンテンツとしてポストバックします。
  3. ユーザがBotを削除したとき=>期待される結果ですが、私の場合、ユーザモデルには、ボットが連絡先に追加または削除されたかどうかに応じて設定/設定解除されるフィールドIsFriendがあります。私は削除時に特定の文字列を確認することができますが、私は単にOK応答だけをチェックします。
関連する問題