标题 & 代码高亮

This commit is contained in:
gdut-yy
2019-12-28 22:27:33 +08:00
parent 67f3f67fac
commit 893fb65d8c
17 changed files with 684 additions and 682 deletions

View File

@@ -1,4 +1,4 @@
JUnit Internals
# 第 15 章 JUnit Internals
Image
JUnit is one of the most famous of all Java frameworks. As frameworks go, it is simple in conception, precise in definition, and elegant in implementation. But what does the code look like? In this chapter well critique an example drawn from the JUnit framework.
@@ -14,7 +14,7 @@ I could explain it further, but the test cases do a better job. So take a look a
Listing 15-1 ComparisonCompactorTest.java
```java
package junit.tests.framework;
import junit.framework.ComparisonCompactor;
import junit.framework.TestCase;
@@ -116,14 +116,14 @@ Listing 15-1 ComparisonCompactorTest.java
assertEquals(expected:<[S&P50]0> but was:<[]0>, failure);
}
}
```
I ran a code coverage analysis on the ComparisonCompactor using these tests. The code is 100 percent covered. Every line of code, every if statement and for loop, is executed by the tests. This gives me a high degree of confidence that the code works and a high degree of respect for the craftsmanship of the authors.
The code for ComparisonCompactor is in Listing 15-2. Take a moment to look over this code. I think youll find it to be nicely partitioned, reasonably expressive, and simple in structure. Once you are done, then well pick the nits together.
Listing 15-2 ComparisonCompactor.java (Original)
```java
package junit.framework;
public class ComparisonCompactor {
@@ -204,12 +204,12 @@ Listing 15-2 ComparisonCompactor.java (Original)
return fExpected.equals(fActual);
}
}
```
You might have a few complaints about this module. There are some long expressions and some strange +1s and so forth. But overall this module is pretty good. After all, it might have looked like Listing 15-3.
Listing 15-3 ComparisonCompator.java (defactored)
```java
package junit.framework;
public class ComparisonCompactor {
private int ctxt;
@@ -257,21 +257,21 @@ Listing 15-3 ComparisonCompator.java (defactored)
return result;
}
}
```
Even though the authors left this module in very good shape, the Boy Scout Rule2 tells us we should leave it cleaner than we found it. So, how can we improve on the original code in Listing 15-2?
2. See “The Boy Scout Rule” on page 14.
The first thing I dont care for is the f prefix for the member variables [N6]. Todays environments make this kind of scope encoding redundant. So lets eliminate all the fs.
```java
private int contextLength;
private String expected;
private String actual;
private int prefix;
private int suffix;
```
Next, we have an unencapsulated conditional at the beginning of the compact function [G28].
```java
public String compact(String message) {
if (expected == null || actual == null || areStringsEqual())
return Assert.format(message, expected, actual);
@@ -281,9 +281,9 @@ Next, we have an unencapsulated conditional at the beginning of the compact func
String actual = compactString(this.actual);
return Assert.format(message, expected, actual);
}
```
This conditional should be encapsulated to make our intent clear. So lets extract a method that explains it.
```java
public String compact(String message) {
if (shouldNotCompact())
return Assert.format(message, expected, actual);
@@ -296,13 +296,13 @@ This conditional should be encapsulated to make our intent clear. So lets ext
private boolean shouldNotCompact() {
return expected == null || actual == null || areStringsEqual();
}
```
I dont much care for the this.expected and this.actual notation in the compact function. This happened when we changed the name of fExpected to expected. Why are there variables in this function that have the same names as the member variables? Dont they represent something else [N4]? We should make the names unambiguous.
String compactExpected = compactString(expected); String compactActual = compactString(actual);
Negatives are slightly harder to understand than positives [G29]. So lets turn that if statement on its head and invert the sense of the conditional.
```java
public String compact(String message) {
if (canBeCompacted()) {
findCommonPrefix();
@@ -317,13 +317,13 @@ Negatives are slightly harder to understand than positives [G29]. So lets tur
private boolean canBeCompacted() {
return expected != null && actual != null && ! areStringsEqual();
}
```
The name of the function is strange [N7]. Although it does compact the strings, it actually might not compact the strings if canBeCompacted returns false. So naming this function compact hides the side effect of the error check. Notice also that the function returns a formatted message, not just the compacted strings. So the name of the function should really be formatCompactedComparison. That makes it read a lot better when taken with the function argument:
```java
public String formatCompactedComparison(String message) {
The body of the if statement is where the true compacting of the expected and actual strings is done. We should extract that as a method named compactExpectedAndActual. However, we want the formatCompactedComparison function to do all the formatting. The compact function should do nothing but compacting [G30]. So lets split it up as follows:
```java
private String compactExpected;
private String compactActual;
@@ -343,9 +343,9 @@ The body of the if statement is where the true compacting of the expected and ac
compactExpected = compactString(expected);
compactActual = compactString(actual);
}
```
Notice that this required us to promote compactExpected and compactActual to member variables. I dont like the way that the last two lines of the new function return variables, but the first two dont. They arent using consistent conventions [G11]. So we should change findCommonPrefix and findCommonSuffix to return the prefix and suffix values.
```java
private void compactExpectedAndActual() {
prefixIndex = findCommonPrefix();
suffixIndex = findCommonSuffix();
@@ -371,11 +371,11 @@ Notice that this required us to promote compactExpected and compactActual to mem
}
return expected.length() - expectedSuffix;
}
```
We should also change the names of the member variables to be a little more accurate [N1]; after all, they are both indices.
Careful inspection of findCommonSuffix exposes a hidden temporal coupling [G31]; it depends on the fact that prefixIndex is calculated by findCommonPrefix. If these two functions were called out of order, there would be a difficult debugging session ahead. So, to expose this temporal coupling, lets have findCommonSuffix take the prefixIndex as an argument.
```java
private void compactExpectedAndActual() {
prefixIndex = findCommonPrefix();
suffixIndex = findCommonSuffix(prefixIndex);
@@ -392,9 +392,9 @@ Careful inspection of findCommonSuffix exposes a hidden temporal coupling [G31];
}
return expected.length() - expectedSuffix;
}
```
Im not really happy with this. The passing of the prefixIndex argument is a bit arbitrary [G32]. It works to establish the ordering but does nothing to explain the need for that ordering. Another programmer might undo what we have done because theres no indication that the parameter is really needed. So lets take a different tack.
```java
private void compactExpectedAndActual() {
findCommonPrefixAndSuffix();
compactExpected = compactString(expected);
@@ -421,9 +421,9 @@ Im not really happy with this. The passing of the prefixIndex argument is a b
if (expected.charAt(prefixIndex) != actual.charAt(prefixIndex))
break;
}
```
We put findCommonPrefix and findCommonSuffix back the way they were, changing the name of findCommonSuffix to findCommonPrefixAnd Suffix and having it call findCommon-Prefix before doing anything else. That establishes the temporal nature of the two functions in a much more dramatic way than the previous solution. It also points out how ugly findCommonPrefixAndSuffix is. Lets clean it up now.
```java
private void findCommonPrefixAndSuffix() {
findCommonPrefix();
int suffixLength = 1;
@@ -440,12 +440,12 @@ We put findCommonPrefix and findCommonSuffix back the way they were, changing th
return actual.length() - suffixLength < prefixLength ||
expected.length() - suffixLength < prefixLength;
}
```
This is much better. It exposes that the suffixIndex is really the length of the suffix and is not well named. The same is true of the prefixIndex, though in that case “index” and “length” are synonymous. Even so, it is more consistent to use “length.” The problem is that the suffixIndex variable is not zero based; it is 1 based and so is not a true length. This is also the reason that there are all those +1s in computeCommonSuffix [G33]. So lets fix that. The result is in Listing 15-4.
Listing 15-4 ComparisonCompactor.java (interim)
```java
public class ComparisonCompactor {
private int suffixLength;
@@ -489,7 +489,7 @@ Listing 15-4 ComparisonCompactor.java (interim)
expected.length() - contextLength ?
ELLIPSIS : “”);
}
```
We replaced the +1s in computeCommonSuffix with a -1 in charFromEnd, where it makes perfect sense, and two <= operators in suffixOverlapsPrefix, where they also make perfect sense. This allowed us to change the name of suffixIndex to suffixLength, greatly enhancing the readability of the code.
There is a problem however. As I was eliminating the +1s, I noticed the following line in compactString:
@@ -499,7 +499,7 @@ There is a problem however. As I was eliminating the +1s, I noticed the followin
Take a look at it in Listing 15-4. By rights, because suffixLength is now one less than it used to be, I should change the > operator to a >= operator. But that makes no sense. It makes sense now! This means that it didnt use to make sense and was probably a bug. Well, not quite a bug. Upon further analysis we see that the if statement now prevents a zero length suffix from being appended. Before we made the change, the if statement was nonfunctional because suffixIndex could never be less than one!
This calls into question both if statements in compactString! It looks as though they could both be eliminated. So lets comment them out and run the tests. They passed! So lets restructure compactString to eliminate the extraneous if statements and make the function much simpler [G9].
```java
private String compactString(String source) {
return
computeCommonPrefix() +
@@ -508,12 +508,12 @@ This calls into question both if statements in compactString! It looks as though
DELTA_END +
computeCommonSuffix();
}
```
This is much better! Now we see that the compactString function is simply composing the fragments together. We can probably make this even clearer. Indeed, there are lots of little cleanups we could do. But rather than drag you through the rest of the changes, Ill just show you the result in Listing 15-5.
Listing 15-5 ComparisonCompactor.java (final)
```java
package junit.framework;
public class ComparisonCompactor {
@@ -616,7 +616,7 @@ Listing 15-5 ComparisonCompactor.java (final)
return (suffixLength > contextLength ? ELLIPSIS : “”);
}
}
```
This is actually quite pretty. The module is separated into a group of analysis functions and another group of synthesis functions. They are topologically sorted so that the definition of each function appears just after it is used. All the analysis functions appear first, and all the synthesis functions appear last.
If you look carefully, you will notice that I reversed several of the decisions I made earlier in this chapter. For example, I inlined some extracted methods back into formatCompactedComparison, and I changed the sense of the shouldNotBeCompacted expression. This is typical. Often one refactoring leads to another that leads to the undoing of the first. Refactoring is an iterative process full of trial and error, inevitably converging on something that we feel is worthy of a professional.