ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 02.01.2026
Просмотров: 3465
Скачиваний: 0
Section 14.6 |
Chapter 14 · Assertions and Unit Testing |
305 |
Like ScalaTest, specs provides a matchers DSL. You can see some examples of specs matchers in action in Listing 14.8 in the lines that contain “must be_==” and “must throwA”. You can use specs standalone, but it is also integrated with ScalaTest and JUnit, so you can run specs tests with those tools as well.3
14.6 Property-based testing
Another useful testing tool for Scala is ScalaCheck, an open source framework written by Rickard Nilsson. ScalaCheck enables you to specify properties that the code under test must obey. For each property, ScalaCheck will generate test data and run tests that check whether the property holds. Listing 14.9 show an example of using ScalaCheck from a ScalaTest WordSpec that mixes in trait Checkers:
import org.scalatest.WordSpec import org.scalatest.prop.Checkers import org.scalacheck.Prop._ import Element.elem
class ElementSpec extends WordSpec with Checkers {
"elem result" must { "have passed width" in {
check((w: Int) => w > 0 ==> (elem('x', w, 3).width == w))
}
"have passed height" in {
check((h: Int) => h > 0 ==> (elem('x', 2, h).height == h))
}
}
}
Listing 14.9 · Writing property-based tests with ScalaCheck.
WordSpec is a ScalaTest trait that provides syntax similar to a specs Specification. The Checkers trait provides several check methods that allow you to mix ScalaCheck property-based tests with traditional assertion-
3You can download specs from http://code.google.com/p/specs/.
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 14.7 |
Chapter 14 · Assertions and Unit Testing |
306 |
or matcher-based tests. In this example, we check two properties that the elem factory should obey. ScalaCheck properties are expressed as function values that take as parameters the required test data, which will be generated by ScalaCheck. In the first property shown in Listing 14.9, the test data is an integer named w that represents a width. Inside the body of the function, you see this code:
w > 0 ==> (elem('x', w, 3).width == w)
The ==> symbol is a ScalaCheck implication operator. It implies that whenever the left hand expression is true, the expression on the right must hold true. Thus in this case, the expression on the right of ==> must hold true whenever w is greater than 0. The right-hand expression in this case will yield true if the width passed to the elem factory is the same as the width of the Element returned by the factory.
With this small amount of code, ScalaCheck will generate possibly hundreds of values for w and test each one, looking for a value for which the property doesn’t hold. If the property holds true for every value ScalaCheck tries, the test will pass. Otherwise, the test will complete abruptly with an AssertionError that contains information including the value that caused the failure.
14.7 Organizing and running tests
Each framework mentioned in this chapter provides some mechanism for organizing and running tests. In this section, we’ll give a quick overview of ScalaTest’s approach. To get the full story on any of these frameworks, however, you’ll need to consult their documentation.
In ScalaTest, you organize large test suites by nesting Suites inside Suites. When a Suite is executed, it will execute its nested Suites as well as its tests. The nested Suites will in turn execute their nested Suites, and so on. A large test suite, therefore, is represented as a tree of Suite objects. When you execute the root Suite in the tree, all Suites in the tree will be executed.
You can nest suites manually or automatically. To nest manually, you either override the nestedSuites method on your Suites, or pass the Suites you want to nest to the constructor of class SuperSuite, which ScalaTest provides for this purpose. To nest automatically, you provide package names
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 14.7 |
Chapter 14 · Assertions and Unit Testing |
307 |
Figure 14.1 · ScalaTest’s graphical reporter.
to ScalaTest’s Runner, which will discover Suites automatically, nest them under a root Suite, and execute the root Suite.
You can invoke ScalaTest’s Runner application from the command line or an ant task. You must specify which suites you want to run, either by naming the suites explicitly or indicating name prefixes with which you want Runner to perform automatic discovery. You can optionally specify a runpath, a list of directories and JAR files from which to load class files for the tests and the code they exercise.4 You can also specify one or more reporters, which will determine how test results will be presented.
For example, the ScalaTest distribution includes the suites that test ScalaTest itself. You can run one of these suites, SuiteSuite,5 with the following command:
$ scala -cp scalatest-1.2.jar org.scalatest.tools.Runner
-p "scalatest-1.2-tests.jar" -s org.scalatest.SuiteSuite
With -cp you place ScalaTest’s JAR file on the class path. The next token, org.scalatest.tools.Runner, is the fully qualified name of the Runner
4Tests can be anywhere on the runpath or classpath, but typically you would keep your tests separate from your production code, in a separate directory hierarchy that mirrors your source tree’s directory hierarchy.
5SuiteSuite is so-named because it is a suite of tests that test trait Suite itself.
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 14.8 |
Chapter 14 · Assertions and Unit Testing |
308 |
application. Scala will run this application and pass the remaining tokens as command line arguments. The -p specifies the runpath, which in this case is a JAR file that contains the suite classes: scalatest-1.2-tests.jar. The -s indicates SuiteSuite is the suite to execute. Because you don’t explicitly specify a reporter, you will by default get the graphical reporter. The result is shown in Figure 14.1.
14.8 Conclusion
In this chapter you saw examples of mixing assertions directly in production code as well as writing them externally in unit tests. You saw that as a Scala programmer, you can take advantage of popular testing tools from the Java community, such as JUnit and TestNG, as well as newer tools designed explicitly for Scala, such as ScalaTest, ScalaCheck, and specs. Both in-code assertions and unit testing can help you achieve your software quality goals. We felt that these techniques are important enough to justify the short detour from the Scala tutorial that this chapter represented. In the next chapter, however, we’ll return to the language tutorial and cover a very useful aspect of Scala: pattern matching.
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Chapter 15
Case Classes and Pattern Matching
This chapter introduces case classes and pattern matching, twin constructs that support you when writing regular, non-encapsulated data structures. These two constructs are particularly helpful for tree-like recursive data.
If you have programmed in a functional language before, then you will probably recognize pattern matching. Case classes will be new to you, though. Case classes are Scala’s way to allow pattern matching on objects without requiring a large amount of boilerplate. In the common case, all you need to do is add a single case keyword to each class that you want to be pattern matchable.
This chapter starts with a simple example of case classes and pattern matching. It then goes through all of the kinds of patterns that are supported, talks about the role of sealed classes, discusses the Option type, and shows some non-obvious places in the language where pattern matching is used. Finally, a larger, more realistic example of pattern matching is shown.
15.1 A simple example
Before delving into all the rules and nuances of pattern matching, it is worth looking at a simple example to get the general idea. Let’s say you need to write a library that manipulates arithmetic expressions, perhaps as part of a domain-specific language you are designing.
A first step to tackle this problem is the definition of the input data. To keep things simple, we’ll concentrate on arithmetic expressions consisting of variables, numbers, and unary and binary operations. This is expressed by the hierarchy of Scala classes shown in Listing 15.1.
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 15.1 |
Chapter 15 · Case Classes and Pattern Matching |
310 |
abstract class Expr
case class Var(name: String) extends Expr case class Number(num: Double) extends Expr
case class UnOp(operator: String, arg: Expr) extends Expr case class BinOp(operator: String,
left: Expr, right: Expr) extends Expr
Listing 15.1 · Defining case classes.
The hierarchy includes an abstract base class Expr with four subclasses, one for each kind of expression being considered.1 The bodies of all five classes are empty. As mentioned previously, in Scala you can leave out the braces around an empty class body if you wish, so class C is the same as class C {}.
Case classes
The other noteworthy thing about the declarations of Listing 15.1 is that each subclass has a case modifier. Classes with such a modifier are called case classes. Using the modifier makes the Scala compiler add some syntactic conveniences to your class.
First, it adds a factory method with the name of the class. This means you can write say, Var("x") to construct a Var object instead of the slightly longer new Var("x"):
scala> val v = Var("x") v: Var = Var(x)
The factory methods are particularly nice when you nest them. Because there are no noisy new keywords sprinkled throughout the code, you can take in the expression’s structure at a glance:
scala> val op = BinOp("+", Number(1), v) op: BinOp = BinOp(+,Number(1.0),Var(x))
The second syntactic convenience is that all arguments in the parameter list of a case class implicitly get a val prefix, so they are maintained as fields:
1Instead of an abstract class, we could have equally well chosen to model the root of that class hierarchy as a trait. Modeling it as an abstract class may be slightly more efficient.
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 15.1 |
Chapter 15 · Case Classes and Pattern Matching |
311 |
scala> v.name res0: String = x
scala> op.left
res1: Expr = Number(1.0)
Third, the compiler adds “natural” implementations of methods toString, hashCode, and equals to your class. They will print, hash, and compare a whole tree consisting of the class and (recursively) all its arguments. Since == in Scala always delegates to equals, this means that elements of case classes are always compared structurally:
scala> println(op) BinOp(+,Number(1.0),Var(x))
scala> op.right == Var("x") res3: Boolean = true
Finally, the compiler adds a copy method to your class for making modified copies. This method is useful for making a new instance of the class that is the same as another one except that one or two attributes are different. The method works by using named and default parameters (Section 8.8). You specify the changes you’d like to make by using named parameters. For any parameter you don’t specify, the value from the old object is used. As an example, here is how you can make an operation just like op except that the operator has changed:
scala> op.copy(operator = "-")
res4: BinOp = BinOp(-,Number(1.0),Var(x))
All these conventions add a lot of convenience, at a small price. The price is that you have to write the case modifier and that your classes and objects become a bit larger. They are larger because additional methods are generated and an implicit field is added for each constructor parameter. However, the biggest advantage of case classes is that they support pattern matching.
Pattern matching
Say you want to simplify arithmetic expressions of the kinds just presented. There is a multitude of possible simplification rules. The following three rules just serve as an illustration:
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 15.1 |
Chapter 15 · Case Classes and Pattern Matching |
312 |
UnOp("-", UnOp("-", e)) => e |
// Double |
negation |
|||
BinOp("+", e, |
Number(0)) => e |
// |
Adding |
zero |
|
BinOp("*", e, |
Number(1)) => e |
// |
Multiplying |
by one |
|
Using pattern matching, these rules can be taken almost as they are to form the core of a simplification function in Scala, as shown in Listing 15.2. The function, simplifyTop, can be used like this:
scala> simplifyTop(UnOp("-", UnOp("-", Var("x"))))
res4: Expr = Var(x)
def simplifyTop(expr: Expr): Expr = expr match {
case UnOp("-", UnOp("-", e)) => e |
// Double |
negation |
||
case BinOp("+", e, Number(0)) => e |
// Adding |
zero |
|
|
case |
BinOp("*", e, Number(1)) => e |
// Multiplying |
by one |
|
case |
_ => expr |
|
|
|
}
Listing 15.2 · The simplifyTop function, which does a pattern match.
The right-hand side of simplifyTop consists of a match expression. match corresponds to switch in Java, but it’s written after the selector expression. I.e., it’s:
selector match { alternatives }
instead of:
switch (selector) { alternatives }
A pattern match includes a sequence of alternatives, each starting with the keyword case. Each alternative includes a pattern and one or more expressions, which will be evaluated if the pattern matches. An arrow symbol => separates the pattern from the expressions.
A match expression is evaluated by trying each of the patterns in the order they are written. The first pattern that matches is selected, and the part following the arrow is selected and executed.
A constant pattern like "+" or 1 matches values that are equal to the constant with respect to ==. A variable pattern like e matches every value. The variable then refers to that value in the right hand side of the case clause.
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index