IDE基于Eclipse,基于Java8
测试类一般在类名上添加Test。
执行单元测试:Package Explorer▶right-click▶Run As ▶ JUnit Test
注意Fails和Errors的区别。
@FunctionalInterface public interface Scoreable { int getScore(); }
import java.util.*; public class ScoreCollection { private List<Scoreable> scores = new ArrayList<>(); public void add(Scoreable scoreable) { scores.add(scoreable); } public int arithmeticMean() { int total = scores.stream().mapToInt(Scoreable::getScore).sum(); return total / scores.size(); } }
测试代码:
import static org.junit.Assert.*; import org.junit.After; import org.junit.Before; import org.junit.Test; import static org.hamcrest.CoreMatchers.*; public class ScoreCollectionTest { @Before public void setUp() throws Exception { } @After public void tearDown() throws Exception { } @Test public void answersArithmeticMeanOfTwoNumbers() { // Arrange ScoreCollection collection = new ScoreCollection(); collection.add(() -> 5); collection.add(() -> 7); // Act int actualResult = collection.arithmeticMean(); // Assert assertThat(actualResult, equalTo(6)); } @Test public void testArithmeticMean() { fail("Not yet implemented"); } }
面试问卷调查系统
public class Answer { private int i; private Question question; public Answer(Question question, int i) { this.question = question; this.i = i; } public Answer(Question characteristic, String matchingValue) { this.question = characteristic; this.i = characteristic.indexOf(matchingValue); } public String getQuestionText() { return question.getText(); } @Override public String toString() { return String.format("%s %s", question.getText(), question.getAnswerChoice(i)); } public boolean match(int expected) { return question.match(expected, i); } public boolean match(Answer otherAnswer) { return question.match(i, otherAnswer.i); } public Question getCharacteristic() { return question; } }
public enum Bool { False(0), True(1); public static final int FALSE = 0; public static final int TRUE = 1; private int value; private Bool(int value) { this.value = value; } public int getValue() { return value; } }
public class BooleanQuestion extends Question { public BooleanQuestion(int id, String text) { super(id, text, new String[] { "No", "Yes" }); } @Override public boolean match(int expected, int actual) { return expected == actual; } }
import java.util.*; public class Criteria implements Iterable<Criterion> { private List<Criterion> criteria = new ArrayList<>(); public void add(Criterion criterion) { criteria.add(criterion); } @Override public Iterator<Criterion> iterator() { return criteria.iterator(); } public int arithmeticMean() { return 0; } public double geometricMean(int[] numbers) { int totalProduct = Arrays.stream(numbers).reduce(1, (product, number) -> product * number); return Math.pow(totalProduct, 1.0 / numbers.length); } }
public class Criterion implements Scoreable { private Weight weight; private Answer answer; private int score; public Criterion(Answer answer, Weight weight) { this.answer = answer; this.weight = weight; } public Answer getAnswer() { return answer; } public Weight getWeight() { return weight; } public void setScore(int score) { this.score = score; } public int getScore() { return score; } }
public class PercentileQuestion extends Question { public PercentileQuestion(int id, String text, String[] answerChoices) { super(id, text, answerChoices); } @Override public boolean match(int expected, int actual) { return expected <= actual; } }
import java.util.*; import java.util.stream.*; public class Person { private List<Question> characteristics = new ArrayList<>(); public void add(Question characteristic) { characteristics.add(characteristic); } public List<Question> getCharacteristics() { return characteristics; } public List<Question> withCharacteristic(String questionPattern) { return characteristics.stream().filter(c -> c.getText().endsWith(questionPattern)).collect(Collectors.toList()); } } /* // your answer // their answer // how important is it to you me very organized you very organized very important me no you no irrelevant 0 little 1 10 50 mandatory 250 how much did other person satisfy? multiply scores take nth root .98 * .94 take sqrt (2 questions) (geometric mean) */
import java.util.*; public class Profile { private Map<String,Answer> answers = new HashMap<>(); private int score; private String name; public Profile(String name) { this.name = name; } public String getName() { return name; } public void add(Answer answer) { answers.put(answer.getQuestionText(), answer); } public boolean matches(Criteria criteria) { score = 0; boolean kill = false; boolean anyMatches = false; for (Criterion criterion: criteria) { Answer answer = answers.get( criterion.getAnswer().getQuestionText()); boolean match = criterion.getWeight() == Weight.DontCare || answer.match(criterion.getAnswer()); if (!match && criterion.getWeight() == Weight.MustMatch) { kill = true; } if (match) { score += criterion.getWeight().getValue(); } anyMatches |= match; } if (kill) return false; return anyMatches; } public int score() { return score; } }
public abstract class Question { private String text; private String[] answerChoices; private int id; public Question(int id, String text, String[] answerChoices) { this.id = id; this.text = text; this.answerChoices = answerChoices; } public String getText() { return text; } public String getAnswerChoice(int i) { return answerChoices[i]; } public boolean match(Answer answer) { return false; } abstract public boolean match(int expected, int actual); public int indexOf(String matchingAnswerChoice) { for (int i = 0; i < answerChoices.length; i++) if (answerChoices[i].equals(matchingAnswerChoice)) return i; return -1; } }
public enum Weight { MustMatch(Integer.MAX_VALUE), VeryImportant(5000), Important(1000), WouldPrefer(100), DontCare(0); private int value; Weight(int value) { this.value = value; } public int getValue() { return value; } }
其他文件和前面类似。
单元测试关注分支和潜在影响数据变化的代码。关注循环,if语句和复杂的条件句。如果值null或零?数据值如何影响的条件判断。
比如上面的Profile类需要关注:
for (Criterion criterion: criteria) 需要考虑Criteria无对象、很多对象的情况。
Answer answer = answers.get(criterion.getAnswer().getQuestionText()); 返回null的情况。 criterion.getAnswer()返回null或 criterion.getAnswer().getQuestionText()。
30行match返回true/false。类似的有34、37、42、44行。
两个分支的测试实例如下:
import org.junit.*; import static org.junit.Assert.*; public class ProfileTest { private Profile profile; private BooleanQuestion question; private Criteria criteria; @Before public void create() { profile = new Profile("Bull Hockey, Inc."); question = new BooleanQuestion(1, "Got bonuses?"); criteria = new Criteria(); } @Test public void matchAnswersFalseWhenMustMatchCriteriaNotMet() { profile.add(new Answer(question, Bool.FALSE)); criteria.add( new Criterion(new Answer(question, Bool.TRUE), Weight.MustMatch)); boolean matches = profile.matches(criteria); assertFalse(matches); } @Test public void matchAnswersTrueForAnyDontCareCriteria() { profile.add(new Answer(question, Bool.FALSE)); criteria.add( new Criterion(new Answer(question, Bool.TRUE), Weight.DontCare)); boolean matches = profile.matches(criteria); assertTrue(matches); } }
断言有JUnit和Hamcrest, 前者的断言如下:
语句 | 描述 |
assertTrue([message,] boolean condition) | 确认布尔条件为真。 |
assertFalse([message,] boolean condition) | 确认布尔条件为假 |
assertEquals([message,] expected, actual) | 确认返回等于期望,数组等比较的是地址而非内容。 |
assertEquals([message,] expected, actual, tolerance) | 测试float或double相等。 tolerance为容许的误差。 |
assertNull([message,] object) | 对象为null |
assertNotNull([message,] object) | 对象非null |
assertSame([message,] expected, actual) | 变量指向同一对象. |
assertNotSame([message,] expected, actual) | 变量指向不同对象 |
Hamcrest的断言:assertThat。导入import static org.hamcrest.CoreMatchers.*;
组织测试
AAA(arrange, act, and assert) 可能还有After。
测试行为与方法:关注行为而不是方法,测试要尽量基于业务。
测试代码和产品代码分开。私有内容也尽量要测试。
单个测试方法的测试内容要尽量单一。
测试文档化;名字要能看出目的。比如doingSomeOperationGeneratesSomeResult、someResultOccursUnderSomeCondition、givenSomeContextWhenDoingSomeBehaviorThenSomeResultOccurs等
• Improve any local-variable names.
• Introduce meaningful constants.
• Prefer Hamcrest assertions.
• Split larger tests into smaller, more-focused tests.
• Move test clutter to helper methods and @Before methods.
另外有基于类的BeforeClass and AfterClass @Ignore可以忽略用例。