2013-01-12 80 views
1

我有一个相当简单的类,我试图单元测试。一般来说,我对单元测试非常陌生,我不确定我应该在这里测试什么。单元测试亚马逊S3

我可以弄清楚如何编码的唯一测试用例是stream的空参数。除此之外,我不知道如何测试PutObjectRequest或其他的结果。如果我在这里使用嘲笑,怎么样?

public class AmazonS3Service : IAmazonS3Service 
{ 
    private readonly Uri baseImageUrl; 
    private readonly Uri s3BaseUrl; 
    private readonly string imageBucket; 

    public AmazonS3Service() 
    { 
     imageBucket = ConfigurationManager.AppSettings["S3.Buckets.Images"]; 

     s3BaseUrl = new Uri(ConfigurationManager.AppSettings["S3.BaseAddress"]); 
     baseImageUrl = new Uri(s3BaseUrl, imageBucket); 
    } 

    public Image UploadImage(Stream stream) 
    { 
     if (stream == null) throw new ArgumentNullException("stream"); 
     var key = string.Format("{0}.jpg", Guid.NewGuid()); 

     var request = new PutObjectRequest 
     { 
      CannedACL = S3CannedACL.PublicRead, 
      Timeout = -1, 
      ReadWriteTimeout = 600000, // 10 minutes * 60 seconds * 1000 milliseconds 
      InputStream = stream, 
      BucketName = imageBucket, 
      Key = key 
     }; 

     using (var client = new AmazonS3Client()) 
     { 
      using (client.PutObject(request)) 
      { 
      } 
     } 

     return new Image 
     { 
      UriString = Path.Combine(baseImageUrl.AbsoluteUri, key) 
     }; 
    } 
} 

回答

1

事情我想看看:

  • 模拟配置管理器为桶和URL返回无效数据。 (空,无效的URL,无效的桶)

  • S3是否支持https?如果是这样,嘲笑它,如果没有,嘲笑它,并验证你得到一个有效的错误。

  • 在(内存,文件,其他类型)中传递不同种类的流。

  • 通在不同状态下的流(空流,已经被读取到 最终流,...)

  • 我将允许超时被设置为参数,这样你就可以测试真的很低 超时,看看你回来了什么错误。

  • 我也会测试重复键,只是为了验证错误信息。尽管你使用的是GUID,但你正在存储到一个亚马逊服务器,其他人可以使用S3 API来存储文件,并且理论上可以创建一个似乎是GUID的文件,但可能会在路上产生冲突(不太可能,但可能的话)

+0

其中大部分将被视为集成测试,对吧?另外,我不确定嘲笑https的意思。 –

+0

通过嘲笑https我的意思是嘲笑配置管理器返回HTTPS和HTTP网址。 –

+0

是的,你说的很多都是集成测试,但那是因为你的代码是如何耦合的。 Mike Z的建议提供了一些方法来帮助遏制这种情况。 –

1

你有麻烦的单元测试UploadImage,因为它被连接到许多其他的外部服务和状态。静态调用包括(new)将代码紧密地耦合到特定实现。你的目标应该是重构这些,这样你可以更容易地进行单元测试。此外,请记住,在对此课程进行单元测试后,您仍然需要执行涉及实际使用Amazon S3服务的大型测试,并确保上载正确无误地发生或按预期失败。通过彻底的单元测试,希望你可以减少这些大的和可能昂贵的测试的数量。

删除耦合到AmazonS3Client实现可能会给你测试降压最大的爆炸。我们需要通过拨出new AmazonS3Client来重构。如果没有这个类的接口,那么我会创建一个包装它。然后你需要决定如何注入实现。有许多选项,包括作为方法参数,构造函数参数,属性或工厂。

让我们使用工厂方法,因为它比其他人更有趣,这很直接。为了清晰和可读性,我省略了一些细节。

interface IClientFactory 
{ 
    IAmazonS3Client CreateAmazonClient(); 
} 

interface IAmazonS3Client 
{ 
    PutObjectResponse PutObject(PutObjectRequest request); // I'm guessing here for the signature. 
} 

public class AmazonS3Service : IAmazonS3Service 
{ 
    // snip 
    private IClientFactory factory; 

    public AmazonS3Service(IClientFactory factory) 
    { 
     // snip 
     this.factory = factory; 
    } 

    public Image UploadImage(Stream stream) 
    { 
     if (stream == null) throw new ArgumentNullException("stream"); 
     var key = string.Format("{0}.jpg", Guid.NewGuid()); 

     var request = new PutObjectRequest 
     { 
      CannedACL = S3CannedACL.PublicRead, 
      Timeout = -1, 
      ReadWriteTimeout = 600000, // 10 minutes * 60 seconds * 1000 milliseconds 
      InputStream = stream, 
      BucketName = imageBucket, 
      Key = key 
     }; 

     // call the factory to provide us with a client. 
     using (var client = factory.CreateAmazonClient()) 
     { 
      using (client.PutObject(request)) 
      { 
      } 
     } 

     return new Image 
     { 
      UriString = Path.Combine(baseImageUrl.AbsoluteUri, key) 
     }; 
    } 
} 

单元测试可能是这样的MSTest的:

[TestMethod] 
public void InputStreamSetOnPutObjectRequest() 
{ 
    var factory = new TestFactory(); 
    var service = new AmazonS3Service(factory); 
    using (var stream = new MemoryStream()) 
    { 
     service.UploadImage(stream); 
     Assert.AreEqual(stream, factory.TestClient.Request.InputStream); 
    } 
} 

class TestFactory : IClientFactory 
{ 
    public TestClient TestClient = new TestClient(); 

    public IAmazonS3Client CreateClient() 
    { 
    return TestClient; 
    } 
} 

class TestClient : IAmazonS3Client 
{ 
    public PutObjectRequest Request; 
    public PutObjectResponse Response; 

    public PutObjectResponse PutObject(PutObjectRequest request) 
    { 
    Request = request; 
    return Response; 
    } 
} 

现在,我们有一个测试验证正确的输入流在请求对象送了过来。显然,一个模拟框架将有助于减少很多样板代码来测试这种行为。您可以通过开始为请求对象上的其他属性编写测试来扩展此功能。错误情况是单元测试可以真正发挥作用的地方,因为在生产实现类中它们往往难以或不可能产生。

要完全测试此方法/类的其他场景,还需要传入或模拟其他外部依赖项。 ConfigurationManager直接访问配置文件。这些设置应该被传入。Guid.NewGuid基本上是不受控制的随机性的来源,这对单元测试也是不利的。你可以定义一个IKeySource作为各种服务的关键值提供者,并模拟它或者只是从外部传递密钥。

最后,您应该权衡测试/重构所花费的所有时间,以及它为您提供多少价值。总是可以添加更多图层来隔离越来越多的组件,但每个添加图层的收益递减。

+0

你给出了很多很好的建议。我喜欢ConfigurationManager分离和IKeySource。我正在使用Ninject而不是工厂方法。我明白你的意思了 - 为TestClient添加一个新层将会呈指数级叠加。相反,我认为我可以在这里使用集成测试。 –