@FunctionalInterfacepublic interface Scoreable { int getScore();}
import java.util.*;public class ScoreCollection { private Listscores = 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{ private List criteria = new ArrayList<>(); public void add(Criterion criterion) { criteria.add(criterion); } @Override public Iterator 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 Listcharacteristics = new ArrayList<>(); public void add(Question characteristic) { characteristics.add(characteristic); } public List getCharacteristics() { return characteristics; } public List withCharacteristic(String questionPattern) { return characteristics.stream().filter(c -> c.getText().endsWith(questionPattern)).collect(Collectors.toList()); }}/*// your answer// their answer// how important is it to youme very organizedyou very organizedvery importantme noyou noirrelevant 0little 11050mandatory 250how 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 Mapanswers = 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); }}
断言
测试行为与方法:关注行为而不是方法,测试要尽量基于业务。 测试代码和产品代码分开。私有内容也尽量要测试。 单个测试方法的测试内容要尽量单一。 测试文档化;名字要能看出目的。比如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可以忽略用例。