java代码的可测性
意大利面设计示例
以下代码是一种类似REST的服务,该服务从Amazon的Simple Storage Service(S3)获取文件列表,并将它们显示为指向文件内容的链接列表:
public class S3FilesResource {
AmazonS3Client amazonS3Client;
...
@Path('files')
public String listS3Files() {
StringBuilder html = new StringBuilder('<html><body>');
List<S3ObjectSummary> files = this.amazonS3Client.listObjects('myBucket').getObjectSummaries();
for (S3ObjectSummary file : files) {
String filePath = file.getKey();
if (!filePath.endsWith('')) { exclude directories
html.append('<a href='content?fileName=').append(filePath).append(''>').append(filePath)
.append('<br>');
}
}
return html.append('<body><html>').toString();
}
@Path('content')
public String getContent(@QueryParam('fileName') String fileName) {
throw new UnsupportedOperationException('Not implemented yet');
}
}
为什么代码难以测试?
- 没有接缝可以使我们绕过对S3的外部依赖,因此我们不能影响将什么数据传递给该方法,也不能轻松地使用不同的值对其进行测试。 此外,我们依靠网络连接和S3服务中的正确状态来运行代码。
- 很难感觉到方法的结果,因为它将数据与它们的表示混合在一起。 直接访问数据以验证目录是否被排除并显示期望的文件名会容易得多。 此外,与HTML表示相比,更改核心逻辑的可能性要小得多,但是即使逻辑不变,更改表示也会破坏我们的测试。
我们可以做些什么来改善它?
我们首先按原样测试代码,以确保我们的重构不会破坏任何东西(测试将是脆弱且丑陋的,但这只是暂时的),重构它以打破外部依赖关系并拆分数据和表示,最后重新编写测试。
我们首先编写一个简单的测试:
public class S3FilesResourceTest {
@Test
public void listFilesButNotDirectoriesAsHtml() throws Exception {
S3FilesResource resource = new S3FilesResource(* pass AWS credentials ... *);
String html = resource.listS3Files();
assertThat(html)
.contains('<a href='content?fileName=dirfile1.txt'>dirfile1.txt')
.contains('<a href='content?fileName=diranother.txt'>diranother.txt')
.doesNotContain('dir'); directories should be excluded
assertThat(html.split(quote(''))).hasSize(2 + 1); two links only
}
}
重构设计
这是重构的设计,其中我通过引入Facade / Adapter将代码与S3分离,并拆分了数据处理和渲染:
public interface S3Facade {
List<S3File> listObjects(String bucketName);
}
public class S3FacadeImpl implements S3Facade {
AmazonS3Client amazonS3Client;
@Override
public List<S3File> listObjects(String bucketName) {
List<S3File> result = new ArrayList<S3File>();
List<S3ObjectSummary> files = this.amazonS3Client.listObjects(bucketName).getObjectSummaries();
for (S3ObjectSummary file : files) {
result.add(new S3File(file.getKey(), file.getKey())); later we can use st. else for the display name
}
return result;
}
}
public class S3File {
public final String displayName;
public final String path;
public S3File(String displayName, String path) {
this.displayName = displayName;
this.path = path;
}
}
public class S3FilesResource {
S3Facade amazonS3Client = new S3FacadeImpl();
...
@Path('files')
public String listS3Files() {
StringBuilder html = new StringBuilder('<html><body>');
List<S3File> files = fetchS3Files();
for (S3File file : files) {
html.append('<a href='content?fileName=').append(file.path).append(''>').append(file.displayName)
.append('<br>');
}
return html.append('<body><html>').toString();
}
List<S3File> fetchS3Files() {
List<S3File> files = this.amazonS3Client.listObjects('myBucket');
List<S3File> result = new ArrayList<S3File>(files.size());
for (S3File file : files) {
if (!file.path.endsWith('')) {
result.add(file);
}
}
return result;
}
@Path('content')
public String getContent(@QueryParam('fileName') String fileName) {
throw new UnsupportedOperationException('Not implemented yet');
}
}
在实践中,我将考虑使用Jersey的内置转换功能(带有用于HTML的自定义MessageBodyWriter ),并从listS3Files
返回List<S3File>
。
这是现在测试的样子:
public class S3FilesResourceTest {
private static class FakeS3Facade implements S3Facade {
List<S3File> fileList;
public List<S3File> listObjects(String bucketName) {
return fileList;
}
}
private S3FilesResource resource;
private FakeS3Facade fakeS3;
@Before
public void setUp() throws Exception {
fakeS3 = new FakeS3Facade();
resource = new S3FilesResource();
resource.amazonS3Client = fakeS3;
}
@Test
public void excludeDirectories() throws Exception {
S3File s3File = new S3File('file', 'file.xx');
fakeS3.fileList = asList(new S3File('dir', 'mydir'), s3File);
assertThat(resource.fetchS3Files())
.hasSize(1)
.contains(s3File);
}
** Simplest possible test of listS3Files *
@Test
public void renderToHtml() throws Exception {
fakeS3.fileList = asList(new S3File('file', 'file.xx'));
assertThat(resource.listS3Files())
.contains('file.xx');
}
}
接下来,我将为REST服务实现集成测试,但仍使用FakeS3Facade来验证该服务是否正常运行,并且可以在预期的URL上访问该文件,以及指向文件内容的链接也正常运行。 我还将为真正的S3客户端编写一个集成测试(通过S3FilesResource,但不在服务器上运行),该集成测试仅按需执行,以验证我们的S3凭据正确并且可以访问S3。 (我不想定期执行它,因为外部服务缓慢而脆弱。)
免责声明:上面的服务不是正确使用REST的一个很好的例子,为了简洁起见,我采用了一些简写,它们并不代表良好的代码。
祝您编程愉快,别忘了分享!
参考: 帮助,我的代码不可测! 我需要修复设计吗? 在The Holy Java博客上来自我们的JCG合作伙伴 Jakub Holy。
翻译自: https://www.javacodegeeks.com/2012/09/help-my-code-isnt-testable-do-i-need-to.html
java代码的可测性