坚实的Java单元测试自动化? (JUnit的/ Hamcrest / ...) [英] Solid Java unit test automation? (JUnit/Hamcrest/...)
问题描述
意图
我正在寻找以下内容:
- 单位测试方法
- 我的方法是什么?
- 我做错了什么?
- 我在做什么是不必要的?
- 自动获取尽可能多的费用。
当前环境
- Eclipse 作为IDE
- JUnit 作为测试框架,集成到Eclipse中
- Hamcrest 作为匹配者库,以获得更好的断言可读性
- Google Guava 进行前提条件验证
当前方法
结构
- 每类测试一个测试类
- 以静态嵌套类分组的方法测试
- 测试方法命名以指定行为测试+预期结果
- Java指定的预期异常 指定 ,而不是方法名称
方法
- 注意
null
值 - 注意空白 列表< E>
- 注意空白 String
- 注意空数组
- 注意对象状态不变量由代码改变(post-conditions)
- 方法接受已记录的参数类型
- 边界检查(例如 Integer.MAX_VALUE 等...)
- 通过特定类型记录不变性(例如 Google Guava ImmutableList< E> )
- ...是否有列表?
- 要检查数据库项目的事情(例如CRUD,连接,日志记录,...)
- 要查看多线程代码的事情
- 检查EJB的事情
- ...?
示例代码
是一个展示一些技巧的例子。
MyPath.java
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.Arrays;
import com.google.common.collect.ImmutableList;
public class MyPath {
public static final MyPath ROOT = MyPath.ofComponents(ROOT);
public static final String SEPARATOR =/;
public static MyPath ofComponents(String ... components){
checkNotNull(components);
checkArgument(components.length> 0);
checkArgument(!Arrays.asList(components).contains());
返回新的MyPath(组件);
}
private final ImmutableList< String>组件;
private MyPath(String [] components){
this.components = ImmutableList.copyOf(components);
}
public ImmutableList< String> getComponents(){
返回组件;
}
@Override
public String toString(){
StringBuilder stringBuilder = new StringBuilder();
for(String pathComponent:components){
stringBuilder.append(/+ pathComponent);
}
return stringBuilder.toString();
}
}
MyPathTests.java
import static org.hamcrest.Matchers.is;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.hamcrest.collection.IsEmptyCollection.empty;
import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.hamcrest.core.IsNot.not;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.junit.Assert.assertThat;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import com.google.common.base.Joiner;
@RunWith(Enclosed.class)
public class MyPathTests {
public static class GetComponents {
@Test
public void componentsCorrespondToFactoryArguments(){
String [ ] components = {Test1,Test2,Test3};
MyPath myPath = MyPath.ofComponents(components);
assertThat(myPath.getComponents(),contains(components));
}
}
public static class OfComponents {
@Test
public void acceptableArrayOfComponents(){
MyPath.ofComponents(Test1,Test2 ,Test3);
}
@Test
public void supportsSingleComponent(){
MyPath.ofComponents(Test1);
}
@Test(expected = IllegalArgumentException.class)
public void emptyStringVarArgsThrows(){
MyPath.ofComponents(new String [] {});
}
@Test(expected = NullPointerException.class)
public void nullStringVarArgsThrows(){
MyPath.ofComponents((String [])null);
}
@Test(expected = IllegalArgumentException.class)
public void rejectsInterspersedEmptyComponents(){
MyPath.ofComponents(Test1,,Test2);
}
@Test(expected = IllegalArgumentException.class)
public void rejectsSingleEmptyComponent(){
MyPath.ofComponents();
}
@Test
public void returnsNotNullValue(){
assertThat(MyPath.ofComponents(Test),is(notNullValue()));
}
}
public static class Root {
@Test
public void hasComponents(){
assertThat(MyPath.ROOT.getComponents(),is (不是空的())));
}
@Test
public void hasExactlyOneComponent(){
assertThat(MyPath.ROOT.getComponents(),hasSize(1));
}
@Test
public void hasExactlyOneInboxComponent(){
assertThat(MyPath.ROOT.getComponents(),contains(ROOT));
}
@Test
public void isNotNull(){
assertThat(MyPath.ROOT,is(notNullValue()));
}
@Test
public void toStringIsSlashSeparatedAbsolutePathToInbox(){
assertThat(MyPath.ROOT.toString(),is(equalTo(/ ROOT)));
}
}
public static class ToString {
@Test
public void toStringIsSlashSeparatedPathOfComponents(){
String [] components = {Test1, Test2,Test3};
String expectedPath =
MyPath.SEPARATOR + Joiner.on(MyPath.SEPARATOR).join(components);
assertThat(MyPath.ofComponents(components).toString(),
是(equalTo(expectedPath)));
}
}
@Test
public void testPathCreationFromComponents(){
String [] pathComponentArguments = new String [] {One,Two,Three };
MyPath myPath = MyPath.ofComponents(pathComponentArguments);
assertThat(myPath.getComponents(),contains(pathComponentArguments));
}
}
问题,明确表达
-
有没有一些技术用于构建单元测试?比我上面简单的列表更先进(例如,检查null,检查边界,检查预期的异常等)可能在一本书中可以购买或访问一个URL?
-
一旦我有一种方法可以使用某种类型的参数,我可以获得任何 Eclipse 插件 - 为我的测试生成一个存根?也许使用Java 注解 指定有关该方法的元数据,并让该工具实现我相关的检查? (例如@MustBeLowerCase,@ShouldBeOfSize(n = 3),...)
我觉得很乏味,机器人像是要记住所有这些QA技巧和/或应用它们,我发现它很容易复制和粘贴,我发现它不是自我记录,当我编写的东西,如上所述。诚然,Hamcrest 图书馆在专业化测试类型的大体方向(例如 String 对象,在 文件 对象等),但显然不会自动生成任何测试存根并不反映代码及其属性,并为我准备一个线束。
请帮助我做得更好。
PS
不要告诉我,我只是提供代码,这是一个愚蠢的包装,围绕创建一个路径的概念一个静态工厂方法提供的路径步骤列表请看这是一个完整的例子,但它显示了一个少数的参数验证情况...如果我包含了一个更长的例子,谁会真的读这个帖子? / em>
-
考虑使用 ExpectedException ,而不是
@Test(expected ...
。这是因为如果你想要一个NullPointerException
,并且您的测试会在您的设置中抛出此异常(在调用测试方法之前),您的测试将通过。在ExpectedException
之前,您会在调用该测试方法之前将预期放在预期之中,因此没有机会。另外,ExpectedException
允许您测试异常消息,如果您有两个不同的IllegalArgumentExceptions
可能会被抛出,您需要检查正确的方法。 -
考虑将测试中的方法与设置和验证隔离,这将有助于测试审查和维护。当被测试类上的方法被调用作为安装程序的一部分时,这是特别真实的,这可能会混淆被测方法。我使用以下格式:
public void test(){
// setup
...
// test(通常这个块中只有一行代码)
...
//验证
...
}
-
要查看的图书:清洁代码, JUnit In Action ,以示例的测试驱动开发
清洁代码有一个非常好的测试部分
-
我已经看到(包括什么Eclipse自动生成)测试题目的测试。这有助于审查和维护。例如:
testOfComponents_nullCase
。您的例子是我看到的第一个使用Enclosed
通过测试方法对方法进行分组,这是非常好的。但是,由于@Before
和@After
不会在封闭的测试类之间共享,所以添加了一些开销。 > -
我没有开始使用它,但Guava有一个测试库: guava-testlib 。我没有机会玩,但似乎有一些很酷的东西。例如: NullPointerTest 是引用:
- 每当任何一个*的参数为空时,验证您的方法会抛出{@link * NullPointerException}或{@link UnsupportedOperationException}的测试实用程序。要使用它,必须首先为类使用的参数类型提供有效的默认值*。
评论:我认识到上面的测试只是一个例子,但由于建设性的评论可能是有帮助的在这里你去。
-
在测试
getComponents
列表案例以及。另外,使用IsIterableContainingInOrder
。 -
在测试
ofComponents
,似乎调用getComponents
或toString
来验证它是否正确处理了各种非错误的情况。应该有一个测试,其中没有参数传递给ofComponents
。我看到这是通过ofComponents(new String [] {})
完成的,但为什么不只是做$ code> ofComponents() ?需要测试,其中null
是传递的值之一:ofComponents(blah,null,blah2)
因为这将抛出一个NPE。 -
在测试
ROOT
之前,如前所述,建议调用ROOT.getComponents
一次并对其进行所有三个验证。另外,ItIterableContainingInOrder
所有三个不为空,大小和包含。测试中的是非常昂贵的(虽然它是语言的),我觉得不值得(IMHO)。
-
在测试
toString
时,我觉得隔离测试方法是非常有帮助的。我会写下toStringIsSlashSeparatedPathOfComponents
,如下所示。请注意,我不使用被测课程的常数。这是因为IM HO IM to to to to b should should cause cause。。。。。。。。。。。。b b b b b b b b b b b b b b b b b b b public void toStringIsSlashSeparatedPathOfComponents(){
// setup
String [] components = {Test1,Test2,Test3};
String expectedPath =/+ Joiner.on(/)。join(components);
MyPath path = MyPath.ofComponents(components)
// test
String value = path.toStrign();
//验证
assertThat(value,equalTo(expectedPath));
}
-
封闭
不会运行任何不在内部类的单元测试。因此testPathCreationFromComponents
将不会运行。
最后,使用测试驱动开发。这将确保您的测试通过正确的原因,并将按预期失败。
Intent
I am looking for the following:
- A solid unit testing methodology
- What am I missing from my approach?
- What am I doing wrong?
- What am I doing which is unnecessary?
- A way to get as much as possible done automatically
Current environment
- Eclipse as IDE
- JUnit as a testing framework, integrated into Eclipse
- Hamcrest as a "matchers" library, for better assertion readability
- Google Guava for precondition validation
Current approach
Structure
- One test class per class to test
- Method testing grouped in static nested classes
- Test method naming to specify behaviour tested + expected result
- Expected exceptions specified by Java Annotation, not in method name
Methodology
- Watch out for
null
values - Watch out for empty List<E>
- Watch out for empty String
- Watch out for empty arrays
- Watch out for object state invariants altered by code (post-conditions)
- Methods accept documented parameter types
- Boundary checks (e.g. Integer.MAX_VALUE, etc...)
- Documenting immutability through specific types (e.g. Google Guava ImmutableList<E>)
- ... is there a list for this? Examples of nice-to-have testing lists:
- Things to check in database projects (e.g. CRUD, connectivity, logging, ...)
- Things to check in multithreaded code
- Things to check for EJBs
- ... ?
Sample code
This is a contrived example to show some techniques.
MyPath.java
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.Arrays;
import com.google.common.collect.ImmutableList;
public class MyPath {
public static final MyPath ROOT = MyPath.ofComponents("ROOT");
public static final String SEPARATOR = "/";
public static MyPath ofComponents(String... components) {
checkNotNull(components);
checkArgument(components.length > 0);
checkArgument(!Arrays.asList(components).contains(""));
return new MyPath(components);
}
private final ImmutableList<String> components;
private MyPath(String[] components) {
this.components = ImmutableList.copyOf(components);
}
public ImmutableList<String> getComponents() {
return components;
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
for (String pathComponent : components) {
stringBuilder.append("/" + pathComponent);
}
return stringBuilder.toString();
}
}
MyPathTests.java
import static org.hamcrest.Matchers.is;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.hamcrest.collection.IsEmptyCollection.empty;
import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.hamcrest.core.IsNot.not;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.junit.Assert.assertThat;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import com.google.common.base.Joiner;
@RunWith(Enclosed.class)
public class MyPathTests {
public static class GetComponents {
@Test
public void componentsCorrespondToFactoryArguments() {
String[] components = { "Test1", "Test2", "Test3" };
MyPath myPath = MyPath.ofComponents(components);
assertThat(myPath.getComponents(), contains(components));
}
}
public static class OfComponents {
@Test
public void acceptsArrayOfComponents() {
MyPath.ofComponents("Test1", "Test2", "Test3");
}
@Test
public void acceptsSingleComponent() {
MyPath.ofComponents("Test1");
}
@Test(expected = IllegalArgumentException.class)
public void emptyStringVarArgsThrows() {
MyPath.ofComponents(new String[] { });
}
@Test(expected = NullPointerException.class)
public void nullStringVarArgsThrows() {
MyPath.ofComponents((String[]) null);
}
@Test(expected = IllegalArgumentException.class)
public void rejectsInterspersedEmptyComponents() {
MyPath.ofComponents("Test1", "", "Test2");
}
@Test(expected = IllegalArgumentException.class)
public void rejectsSingleEmptyComponent() {
MyPath.ofComponents("");
}
@Test
public void returnsNotNullValue() {
assertThat(MyPath.ofComponents("Test"), is(notNullValue()));
}
}
public static class Root {
@Test
public void hasComponents() {
assertThat(MyPath.ROOT.getComponents(), is(not(empty())));
}
@Test
public void hasExactlyOneComponent() {
assertThat(MyPath.ROOT.getComponents(), hasSize(1));
}
@Test
public void hasExactlyOneInboxComponent() {
assertThat(MyPath.ROOT.getComponents(), contains("ROOT"));
}
@Test
public void isNotNull() {
assertThat(MyPath.ROOT, is(notNullValue()));
}
@Test
public void toStringIsSlashSeparatedAbsolutePathToInbox() {
assertThat(MyPath.ROOT.toString(), is(equalTo("/ROOT")));
}
}
public static class ToString {
@Test
public void toStringIsSlashSeparatedPathOfComponents() {
String[] components = { "Test1", "Test2", "Test3" };
String expectedPath =
MyPath.SEPARATOR + Joiner.on(MyPath.SEPARATOR).join(components);
assertThat(MyPath.ofComponents(components).toString(),
is(equalTo(expectedPath)));
}
}
@Test
public void testPathCreationFromComponents() {
String[] pathComponentArguments = new String[] { "One", "Two", "Three" };
MyPath myPath = MyPath.ofComponents(pathComponentArguments);
assertThat(myPath.getComponents(), contains(pathComponentArguments));
}
}
Question, phrased explicitly
Is there a list of techniques to use to build a unit test? Something much more advanced than my oversimplified list above (e.g. check nulls, check boundaries, check expected exceptions, etc.) perhaps available in a book to buy or a URL to visit?
Once I have a method that takes a certain type of parameters, can I get any Eclipse plug-in to generate a stub for my tests for me? Perhaps using a Java Annotation to specify metadata about the method and having the tool materialise the associated checks for me? (e.g. @MustBeLowerCase, @ShouldBeOfSize(n=3), ...)
I find it tedious and robot-like to have to remember all of these "QA tricks" and/or apply them, I find it error-prone to copy and paste and I find it not self-documenting when I code things as I do above. Admittedly, Hamcrest libraries go in the general direction of specialising types of tests (e.g. on String objects using RegEx, on File objects, etc) but obviously do not auto-generate any test stubs and do not reflect on the code and its properties and prepare a harness for me.
Help me make this better, please.
PS
Do not tell me that I am just presenting code which is a silly wrapper around the concept of creating a Path from a list of path steps supplied in a static factory method please, this is a totally made-up example but it shows a "few" cases of argument validation... If I included a much longer example, who would really read this post?
Consider using ExpectedException instead of
@Test(expected...
. This is because if for example you expect aNullPointerException
and your test throws this exception in your setup (before calling the method under test) your test will pass. WithExpectedException
you put the expect immediately before the call to the method under test so there is no chance of this. Also,ExpectedException
allows you to test the exception message which is helpful if you have two differentIllegalArgumentExceptions
that might be thrown and you need to check for the correct one.Consider isolating your method under test from the setup and verify, this will ease in test review and maintenance. This is especially true when methods on the class under test are invoked as part of setup which can confuse which is the method under test. I use the following format:
public void test() { //setup ... // test (usually only one line of code in this block) ... //verify ... }
Books to look at: Clean Code, JUnit In Action, Test Driven Development By Example
Clean Code has an excellent section on testing
Most example I have seen (including what Eclipse autogenerates) have the method under test in the title of the test. This facilitates review and maintenance. For example:
testOfComponents_nullCase
. Your example is the first I have seen that uses theEnclosed
to group methods by method under test, which is really nice. However, it adds some overhead as@Before
and@After
do not get shared between enclosed test classes.I have not started using it, but Guava has a test library: guava-testlib. I have not had a chance to play with it but it seems to have some cool stuff. For example: NullPointerTest is quote:
- A test utility that verifies that your methods throw {@link * NullPointerException} or {@link UnsupportedOperationException} whenever any * of their parameters are null. To use it, you must first provide valid default * values for the parameter types used by the class.
Review: I realize the test above was just an example but since a constructive review might be helpful, here you go.
In testing
getComponents
, test the empty list case as well. Also, useIsIterableContainingInOrder
.In testing of
ofComponents
, it seems that it would make sense to callgetComponents
ortoString
to validate that it properly handled the various non-error cases. There should be a test where no argument is passed toofComponents
. I see that this is done withofComponents( new String[]{})
but why not just doofComponents()
? Need a test wherenull
is one of the values passed:ofComponents("blah", null, "blah2")
since this will throw an NPE.In testing
ROOT
, as has been pointed out before, I suggest callingROOT.getComponents
once and doing all three verifications on it. Also,ItIterableContainingInOrder
does all three of not empty, size and contains. Theis
in the tests is extraineous (although it is linguistic) and I feel is not worth having (IMHO).In testing
toString
, I feel it is very helpful to isolate the method under test. I would have writtentoStringIsSlashSeparatedPathOfComponents
as follows. Notice that I do not use the constant from the class under test. This is because IMHO, ANY functional change to the class under test should cause the test to fail.@Test public void toStringIsSlashSeparatedPathOfComponents() { //setup String[] components = { "Test1", "Test2", "Test3" }; String expectedPath = "/" + Joiner.on("/").join(components); MyPath path = MyPath.ofComponents(components) // test String value = path.toStrign(); // verify assertThat(value, equalTo(expectedPath)); }
Enclosed
will not run any unit test that is not in an inner class. ThereforetestPathCreationFromComponents
would not be run.
Finally, use Test Driven Development. This will ensure that your tests are passing for the right reason and will fail as expected.
这篇关于坚实的Java单元测试自动化? (JUnit的/ Hamcrest / ...)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!