无法在单元测试中访问已处置的对象

Posted

技术标签:

【中文标题】无法在单元测试中访问已处置的对象【英文标题】:Cannot access a disposed object in unit test 【发布时间】:2021-08-01 18:09:16 【问题描述】:

我有模拟 http 客户端的功能:

private void MockClient(HttpStatusCode status, HttpContent content = null)
 
   _client
     .Setup(m => m.Get(It.IsAny<string>(), It.IsAny<CancellationToken>()))
     .Returns(() => Task.FromResult(new HttpResponseMessage StatusCode = status, Content = content)).Verifiable();
 

我使用它进行这样的测试:

        [Fact]
        public async void Get_SerializerFailure_Throws()
        
            var content = new MemoryStream(new byte[5]);
            MockClient(HttpStatusCode.OK, new StreamContent(content));

            _jsonSerializer.Setup(x => x.DeserializeFromStream<Dto>(It.IsAny<Stream>())).Throws<JsonReaderException>();
            await Assert.ThrowsAsync<JsonReaderException>(() => _sut.GetAllAsync());
        

这里出现错误:

 typeof(System.ObjectDisposedException): Cannot access a disposed object.
Object name: 'System.Net.Http.StreamContent'.

但是,如果我像这样直接在测试中模拟客户端,它可以工作并且不会抛出此错误:

        [Fact]
        public async void Get_ProtobufSerializerFailure_Throws()
        
            var content = new MemoryStream(new byte[5]);
            _client
                .Setup(m => m.Get(It.IsAny<string>(), It.IsAny<CancellationToken>()))
                .Returns(() => Task.FromResult(new HttpResponseMessage StatusCode = HttpStatusCode.OK, Content = new StreamContent(content))).Verifiable();


            _protobufSerializer.Setup(x => x.DeserializeFromStream<Dto>(It.IsAny<Stream>())).Throws<JsonReaderException>();
            await Assert.ThrowsAsync<JsonReaderException>(() => _sut.GetAllAsync());
        

我不明白这里有什么区别以及为什么一种方法有效而另一种方法无效以及如何修复我的模拟方法。

我正在测试这些方法:

private async Task<Object> GetLatest()
        
            using (var response = await _client.Get($"_serviceUrl", CancellationToken.None))
            using (var stream = await response.Content.ReadAsStreamAsync())
            
                return _jsonSerializer.DeserializeFromStream<Objects>(stream)?.Items.Last();
            
        

        public async Task<IReadOnlyList<(ulong key, Dto item)>> GetAllAsync()
        
            var latest = await GetLatest();

            using (var response = await _client.Get(url,CancellationToken.None))
            using (var stream = await response.Content.ReadAsStreamAsync())
            using (var decompressionStream = new GZipStream(stream, CompressionMode.Decompress))
            
                var result = _protobufSerializer.DeserializeFromStream<Dto>(decompressionStream);
                return result.ToList();
            
        

【问题讨论】:

首先避免使用async void,使用async Task。其次,展示正在测试的主题,以便我们更好地了解您要测试的内容 更新问题@Nkosi 问题出在范围上。在第一个中,流内容作为变量传递给返回函数,并将在Get 第一次被GetLatest 调用并且响应被处理时被处理。在第二个示例中,每次调用返回函数时都会创建一个新的内容流。 但是在我运行程序的时候效果很好,但是在单元测试中就不行了 因为在程序中实际客户端返回不同的响应。在您的单元测试中,您的模拟每次都返回相同的客户端响应内容。 【参考方案1】:

这两种测试设置的不同之处在于在测试执行期间创建了多少个StreamContent 类实例。

第一个设置创建HttpResponseMessage 类的多个实例,并引用作为参数传递给MockClient 方法的StreamContent 的同一单个实例。
.Returns(() => Task.FromResult(new HttpResponseMessage StatusCode = status,
    Content = content))
第二个设置还创建了多个HttpResponseMessage 类实例,每个_client.Get 方法执行一个,但每个HttpResponseMessage 实例都有自己的StreamContent 实例。
.Returns(() => Task.FromResult(new HttpResponseMessage StatusCode = HttpStatusCode.OK,
    Content = new StreamContent(content)))

在提供的代码中,_client.Get 方法在单个测试执行期间被调用了两次。第一次在GetLatest 方法中,第二次在GetAllAsync 方法中。每个方法读取响应内容并处理它。因此,在第一次设置的情况下,GetAllAsync 方法会在尝试将响应内容作为流读取时收到 ObjectDisposedException,因为响应内容的单个实例已经在 GetLatest 方法中进行了处理。

所有一次性对象的创建次数应与在测试代码中检索和处置的次数相同。


解决方案 1

作为一种可能的解决方案,应将 MockClient 方法更改为接受字节数组而不是 StreamContent,并为每个 HttpResponseMessage 响应创建单独的 StreamContent 和 MemoryStream 实例。

private void MockClient(HttpStatusCode status, byte[] content = null)

   _client
      .Setup(m => m.Get(It.IsAny<string>(), It.IsAny<CancellationToken>()))
      .Returns(() => Task.FromResult(
         new HttpResponseMessage
         
            StatusCode = status, 
            Content = content == null 
               ? null 
               : new StreamContent(new MemoryStream(content))
         ))
      .Verifiable();


[Fact]
public async Task Get_SerializerFailure_Throws()

   MockClient(HttpStatusCode.OK, new byte[5]);

   _jsonSerializer.Setup(x => x.DeserializeFromStream<Dto>(It.IsAny<Stream>())).Throws<JsonReaderException>();
   await Assert.ThrowsAsync<JsonReaderException>(() => _sut.GetAllAsync());



解决方案 2

另一种选择是将Func&lt;StreamContent&gt; 委托传递给MockClient 方法。

private void MockClient(HttpStatusCode status, Func<StreamContent> content = null)

   _client
      .Setup(m => m.Get(It.IsAny<string>(), It.IsAny<CancellationToken>()))
      .Returns(() => Task.FromResult(
         new HttpResponseMessage
         
            StatusCode = status, 
            Content = content == null 
               ? null 
               : content()
         ))
      .Verifiable();


[Fact]
public async Task Get_SerializerFailure_Throws()

   MockClient(HttpStatusCode.OK, () => new StreamContent(new MemoryStream(new byte[5])));

   _jsonSerializer.Setup(x => x.DeserializeFromStream<Dto>(It.IsAny<Stream>())).Throws<JsonReaderException>();
   await Assert.ThrowsAsync<JsonReaderException>(() => _sut.GetAllAsync());


【讨论】:

以上是关于无法在单元测试中访问已处置的对象的主要内容,如果未能解决你的问题,请参考以下文章

集成测试框架

无法访问已处置的对象 - 如何修复?

VSTestHost.exe 已停止工作 - 无法运行单元测试

在单元测试中访问具有相同类型对象的两个不同列表

全新的 Xcode 单元测试目标无法运行,因为“启动会话在签入前已过期”。

System.ObjectDisposedException:'无法访问已处置的对象。对象名称:'OracleConnection'。'