Adam Rosien @arosien
Inner Product LLC inner-product.com
17 Oct 2019
( ಠ ʖ̯ ಠ)
Install sbt
: https://www.scala-sbt.org/release/docs/Setup.html
Clone repo: github.com/inner-product/essential-testing-code
$ git clone git@github.com:inner-product/essential-testing-code.git
$ cd essential-testing-code
$ sbt
sbt:essential-testing-code> testOnly AbsSpec
[info] ! Abs.non-negative: Falsified after 3 passed tests.
[info] > ARG_0: -2147483648
[info] Failed: Total 1, Failed 1, Errors 0, Passed 0
...
what do they do?
why are we here?
¿ⓧ_ⓧﮌ
write code that doesn’t compile
change the code
when the code compiles, done!
👍
make a failing test
change the code
when the test passes, done!
Program testing can be used to show the presence of bugs, but never to show their absence!
Edsger Dijkstra
Types
Pros: correct for all inputs; compiler-checked
Cons: doesn’t handle all properties; the compiler can be annoying
Tests
Pros: can prove complex properties we cannot express in types
Cons: more effort, more boilerplate; test inputs are cherry-picked; can only prove the presence of bugs
trait TestFixture {
val inputs = List(
DoSomethingInput(12, "yes", true, true, false),
...
)
case class DoSomethingInput(i: Int, s: String, b1: Boolean, b2: Boolean, b3: Boolean)
}
extract the test inputs into a fixture
class MyTest extends SomeTestThing with TestFixture {
val systemUnderTest = ???
test("my code") {
for {
input <- inputs
(i, s, b1, b2, b3) = input
} assert(systemUnderTest.doSomething(
i, s, b1, b2, b3) == null)
}
}
assert over all fixture inputs
what problems still exist with this style?
example-generation: test fixture → generators
assertion specification: assertions → properties
x + y
what properties can you think of?
x
and y
, z = x + y
is also an integerx + 0 = x = 0 + x
(x + y) + z = x + (y + z)
x + y = y + x
x
there is an integer -x
such that x + -x = 0
Int
Additionsame properties?
Assertions are boolean formulas over a concrete type:
Properties are universally quantified over a concrete type:
Laws are universally quantified over an abstract type:
Preconditions: additional restriction on the input
when the type doesn’t express this restriction itself
length
and size
in the standard library)++
in the standard library)sorted
in the standard library)patterns: closure, identity, associativity, commutativity, invertibility, …
import org.scalacheck.Gen
Gen.alphaStr
Gen.numStr
Gen.posNum[Int]
Gen.const(1)
Gen.choose(0, 9)
Gen.oneOf('a', 'b', 'c')
and more at https://www.scalacheck.org/files/scalacheck_2.11-1.14.0-api/index.html#org.scalacheck.Gen$
(or pass generators as the values)
String
of lowercase alphabetic charactersString
of numeric charactersBuilding blocks available at https://www.scalacheck.org/files/scalacheck_2.11-1.14.0-api/index.html#org.scalacheck.Gen$ (yes, terrible URL)
Create a Gen[User]
, where User
is
Make justifiable decisions for the choice of generators for the fields.
Generators are a source of feedback!
too “wide”
List => NonEmptyList
, etc.too “narrow”
>>> POST /todos?value=get+milk&due=2018-05-13 HTTP/1.1
<<< HTTP/1.1 201 Created
<<< Location: /todos/68
>>> GET /todos/68
<<< HTTP/1.1 200 OK
<<<
<<< {
<<< "value": "get milk"
<<< "due": "2018-05-13"
<<< }
>>> DELETE /todos/68
<<< HTTP/1.1 204 No Content
POST /todos?value=<value>&due=<due>
GET /todos/<id>
DELETE /todos/<id>
What properties should hold?
val c = new Counter
// c: Counter = repl.Session$App3$Counter@2b42ed66
c.inc
c.inc
c.get
// res23: Int = 2
How do we know the counter is incremented correctly, no matter what commands are given to it?
Exercise: Think about what properties we want to prove, and what state we need to keep track of in the test in order to to make these assertions.
For our counter, we need to:
Gen
, commands that represent calls to the inc
, dec
, get
, and reset
methods.get
, we need to assert if our computed value of the counter matches what the counter outputs.See stateful/CounterProperties.scala
trait ATM {
def authorize(user: User, pass: Password): Boolean
def balance(user: User): Either[ATM.Error, Amount]
def withdraw(user: User, amount: Double): Either[ATM.Error, Amount]
}
(See stateful/ATM.scala
)
What state is hidden, that our test should track?
What pre- or post-conditions should we assert after running any of these commands?
Work with the stateful test template stateful/ATMProperties.scala
, get it to compile by replacing all ???
implementations with real implementations.
Extra exercise: Add a deposit
method and update the tests
Tests: examples checked via assertions
Tests prove the presence of bugs.
Properties can’t be proven, but they can be falsified.
Don’t write examples, write example-generating programs!
Adam Rosien @arosien
Inner Product LLC inner-product.com
Hire us to teach your team! ☝︎
References and Links: