Documentation

Introduction


Xtest is a unit-testing domain-specific language for Java. It is very similar to Java except eliminates boilerplate not needed for testing and makes unit-testing concepts part of the language. Xtest also gives you access to the internal state of objects so that you don't need to change your API to test your code.

Xtest integrates tightly with Eclipse. The top-notch editor with syntax-highlighting and autocomplete makes writing Xtest feel just like writing Java, but with a twist. Test failures are highlighted in the file just like compile errors in Java files.

Don't hesitate to open a github issue for any problems found or suggestions.

Installation


Xtest requires Eclipse SDK 3.6 or above. You can install the latest version of Xtest from the following update site:

http://msbarry.github.com/Xtest/updates/release

Note If you already have Xtext installed or want to download the update site as a zip file, follow the special installation instructions on the download page.

Creating an Xtest file


Before creating an Xtest file in a new or existing Java project, you need to add the Xtest runtime libraries to its build path.

Right click on the Java project and select Build Path -> Add Libraries... then select Xtest Libraries from the Add Library Wizard.

Now add a new Xtest file by right clicking on a folder or package and selecting New -> Other... then Xtest File under the Xtest category.

Then in the wizard that pops up give the file a name (the .xtest extension will be added automatically) and change the target folder and package if necessary.

New Xtest file wizard

Click Finish and answer Yes when you are prompted to add the Xtext nature to your project. A new Xtest file will be created with a default template xtest file with one test case containing one assertion.

xtest test {
    assert true
}

The Editing Environment


After you have created a new Xtest file, Eclipse displays a number of indicators that there is a new Xtest file and it is passing:

Change the contents of the default Xtest file to this failing test script:

xtest test {
    assert false
}

By the time you finish modifying the script in the Eclipse editor, Eclipse has already run your test from the live document you are editing, behind the scenes, within the main Eclipse process. The assert false expression fails the test and thus is underlined as an error.

Mouse-over that expression and you will see the stack trace of the the exception that was thrown when your assertion failed.

Eclipse editor with a failing test

Save the file to commit your changes to disk. The Xtest runner listens to all file modifications, selects which tests may be affected by those modifications, then runs those tests sorted from fastest to slowest. In this case there is only one test, and since it depends on itself, the Xtest runner schedules it to be run.

When it is first scheduled, the Xtest status bar backs up to 0% complete.

Status bar backs up to 0% complete

Then after the test completes, it returns to 100% complete but turns red because there is now a failure and reads "1F/2" because one test out of two total tests is now failing.

Status bar with 1 failing test out of 2 total tests

Now all parts of Eclipse have updated to show that the test is failing.

Eclipse with all views indicating a failing test

Here is a taste of what the Eclipse editor can look like in a more complex situation:

More complex tests in Eclipse with all views showing failures

Assertions


assert tests that an expression evaluates to true or throws a particular exception. A failed assertion is treated as a test failure that is handled by the surrounding test case. No subsequent expressions in the test case are executed, but expressions after the surrounding test case continue executing normally.

Assert Boolean

assert expression passes if the expression evaluates to true, and fails if it evaluates to anything besides true.

For example, all of the following assertions pass:

assert true
assert 1 == 1
assert 3 < 4
assert "abc".startsWith("ab")

And the following assertions fail:

assert false   // fails
assert 1 == 2  // fails since 1==2 evalutes to false
assert "Bob"   // fails since "Bob" is not a boolean

Assert Throws

assert expression throws ExceptionType passes if and only if the expression throws an exception that is type-compatible with ExceptionType.

For example, the following assertions pass since 1/0 throws an ArithmeticException:

assert 1/0 throws Exception
assert 1/0 throws ArithmeticException

And the following assertions fail:

assert true throws Exception               // "true" throws no exception
assert 1/0 throws IllegalArgumentException // not type-compatible

Complex Expressions

assert expression can take complex expressions. When it fails, AssertionMessageBuilder generates an error message by doing a depth-first traversal through the expression tree to tell you what each part evaluated to so that you can easily diagnose what went wrong.

For example when comparing two variables, the value and type of each variable is shown:

val a = 1
val b = 2
assert a == b
Error!Assertion failed
   "a == b" was Boolean <false>
   "a" was Integer <1>
   "b" was Integer <2>

And when evaluating a complex boolean expression, the results of sub-expressions are shown:

assert 1 < 2 && "abc".startsWith("d") || (3+5) > 10
Error!Assertion failed
   "1 < 2 && "abc".startsWith("d") || (3+5) > 10" was Boolean <false>
   "1 < 2 && "abc".startsWith("d")" was Boolean <false>
   "1 < 2" was Boolean <true>
   ""abc".startsWith("d")" was Boolean <false>
   "(3+5) > 10" was Boolean <false>
   "3+5" was Integer <8>

Exception Handling


Exceptions can be caught using try catch finally blocks as in Java, but when uncaught, the surrounding test case treats them as test failures just like failed assertions. In addition to catching bugs in your Java code, this also lets you use third party libraries to perform your assertions instead of the built-in assert expression.

Example: Bug Catching

val i = 0;
val num = 1/i
Error!java.lang.ArithmeticException: / by zero
Exceptions."1/i"(Exceptions:2)

Third Party Assertion Library Examples

You can use import and import static in Xtest just like in Java to import any third party assertion libraries (or code under test).

Example: JUnit Assertions

import static org.junit.Assert.*

val actual = "abc"
val expected = "abc123"
assertEquals("strings", expected, actual)
Error!org.junit.ComparisonFailure: strings expected:<abc[123]> but was:<abc[]>
org.junit.Assert.assertEquals(Assert.java:125)
Exceptions."assertEquals("strings", expected, actual)"(Exceptions:5)

Example: Hamcrest Matchers

import static org.junit.Assert.*
import static org.hamcrest.CoreMatchers.*

assertThat("abc", is(not(instanceOf(String::class))))
Error!java.lang.AssertionError:
Expected: is not an instance of java.lang.String
     got: "abc"

org.junit.Assert.assertThat(Assert.java:780)
org.junit.Assert.assertThat(Assert.java:738)
Exceptions."assertThat("abc", is(not(instanceOf(String::class))))"(Exceptions:4)

Example: Mockito

import static org.mockito.Mockito.*
import java.util.List

val mockList = mock(List::class)
when(mockList.add("a")).thenThrow(AssertionError::class)
mockList.add(1)
mockList.add("a")
Error!java.lang.AssertionError
Exceptions."mockList.add("a")"(Exceptions:7)

Test Cases


The xtest expression is used to group other expressions into test cases. Failures that occur inside a test case cause it to fail and halt abruptly, but expressions after that test case continue executing normally. Test cases can be nested inside eachother to any depth and the outline view in Eclipse reflects the dynamic result structure generated from evaluating all test cases.

Each file starts off with one implicit file-level test case.

The keyword xtest is used instead of "test" in order to prevent naming collisions with java packages and variables named "test." To use an identifier or java package named xtest, prefix it with the escape character: ^xtest

Test Naming

Test cases names are optional. When no name is specified, the name is computed from the first expression within the test case:

xtest { assert 1 == 1 } // Named "assert 1 == 1"

Tests cases can also be named with a string to specify static names:

xtest "test case" { }   // Named "test case"

Or they can be named by the toString() result of a dynamic object placed in parenthesis:

val num = 1 + 1
xtest ("test " + num) {}  // Named "test 2"

Example: Nested Test Cases

Test cases can be nested to any depth, which is convenient for grouping test cases into suites:

Eclipse outline view with a nested test case

Example: Test Case in For Loop

Since test case is just another expression, it can be used in and around loops and conditionals. In general it is good to stay away from conditional test logic, however this can come in handy for data-driven test input and custom assertion methods.

Eclipse outline view with a test case inside a loop

Methods


Xtest allows you to define re-usable blocks of Xtest code in methods prefixed with the def keyword. For the most part, Xtest methods work like their Java counterparts, but there are a few exceptions as outlined below. You can use methods declared in Xtest exactly the same way as methods declared on Java classes.

Inferred Return Types and Implicit Returns

Xtest can infer the return type of methods automatically. When there is no return expression, the result of the last expression in the method is used as the return value.

For example:

def String sayHello() {
  return "Hello!"
}

Does not need to declare its return type:

def sayHello() {
  return "Hello!"
}

And does not need the return keyword:

def sayHello() {
  "Hello!"
}

And for that matter since it has no arguments, it does not even need ():

def sayHello {
  "Hello!"
}

Var-args and Generics

Xtest methods have the same rules for generics and var-args as Java 5+:

def <T extends Comparable<? super T>> T max(T... args) {
  return args.reduce[cur, next|{
    if (cur > next) cur else next
  }]
}

This method uses a number of Xbase concepts explained later including lambda expressions, extension methods, and overloaded operators.

Local Method Scope

Methods with no modifiers have "local" scope that captures the local variable and method context at the declaration point, much like JavaScript functions or lambda expressions described later:

val List<String> list = newArrayList
def addToList(String value) {
  list.add(value)
}

Note Only final variables (using val not var) declared prior to a local method can be used inside that method.

Since local methods are just expressions, they can be arbitrarily nested:

def outerMethod(String i) {
  def innerMethod(String j) {
    return i+" "+j
  }
  return innerMethod("inner")
}

assert outerMethod("outer") == "outer inner"

Note You cannot access a local method above where it is declared.

Static Method Scope

Methods declared with the static keyword have "static" scope that does not include any local variables or methods just other static methods. They can be used anywhere in the file, before or after their declaration. Static methods of an Xtest file are also available to other Xtest files.

Static methods can be used above where they are declared and within any local method:

def test(String name, String greeting) {
  // Using createGreeting() before it is defined
  assert createGreeting(name) == greeting
}

def static createGreeting(String name) {
  return "Hello " + name + "!"
}

test("Mike", "Hello Mike!")
test("Janice", "Hello Janice!")

Static methods can also be declared in one Xtest file:

// Definition.xtest
def static createGreeting(String name) {
  return "Hello " + name + "!"
}

And used in another:

// Use.xtest
import static Definition.*

assert createGreeting("Mike") == "Hello Mike!"

Other Test-Specific Examples

When writing unit tests it is often useful to encapsulate the API of the software under test behind helper methods so that if the API changes, test code only needs to be changed in one place. As a bonus, if the logic contained in these methods becomes complex it allows tests to be written for them to ensure they behave properly.

Example: Custom Creation Method

Creation methods encapsulate the logic needed to instantiate new objects needed for tests.

def newSUTWithDependency() {
  val dependency = new Dependency("Test Dependency")
  return new SUT("Test SUT", dependency)
}

xtest "SUT test #1" {
  val sut = newSUTWithDependency
  // ...

Example: Custom Assertion Method

assert expressions (or any third party assertions) can be encapsulated into custom assertion methods. Assertion failures are handled by the test case that invokes the custom assertion method.

def <T> assertListsEqual(List<T> l1, List<T> l2) {
  assert l1.size == l2.size
  for (i : 0..(l1.size - 1)) {
    assert l1.get(i) == l2.get(i)
  }
}

Example: Fully Parameterized Test

Entire xtest expressions performing setup, verification, and teardown can be encapsulated into parameterized test methods. The test result from the encapsulated xtest expression gets added as a child to the test case that the invocation occurs in. If the test fails, expressions after the parameterized test method invocation continue executing normally.

def testCheckAmountConversion(BigDecimal input, String output) {
  xtest (input+"->"+output) {
    val checkAmount = new CheckAmount(input)
    val toString = checkAmount.toString
    assert toString == output
  }
}

testCheckAmountConversion(0bd, "Zero and 00/100 dollars")
testCheckAmountConversion(1bd, "One and 01/100 dollars")
testCheckAmountConversion(9.1bd, "Nine and 10/100 dollars")
testCheckAmountConversion(9.11bd, "Nine and 11/100 dollars")

Notice that the second test fails but the third and fourth execute normally. The outline view in Eclipse shows four test cases, one for each method invocation.

Extension Methods


Extension methods allow you to add methods to existing types. Instead of passing the first argument of a method inside the parentheses of a call, you can also call the method on the first argument parameter directly.

For example, the following method defined in an Xtest file:

def <T> first(Iterable<T> iterable) {
  return iterable.iterator().next()
}

Can either be called with an Iterable argument, or can be called directly on an Iterable:

first(iterable)
iterable.first()

You can also write extension methods in Java. The method must be declared static:

// this is Java!
public class Extensions {
  public static String remove(String input, String substring) {
    return input.replace(sustring, "");
  }
}

And they must be imported using import static extension:

// this is Xtest!
import static extension Extensions.*
assert "abaab".remove("a") == "bb"

Any static Java method with one or more arguments can be imported and used as an extension method. For example:

import static extension com.google.common.collect.Sets.*

val setA = newHashSet(1,2,3)
val setB = newHashSet(3,4,5)

assert setA.union(setB) == newHashSet(1,2,3,4,5)
assert setA.intersection(setB) == newHashSet(3)

By default, a number of extension methods provided by the Xtext team in the Xbase Libraries are available implicitly, without any imports.

assert newArrayList(1,2,3).head == 1

Example: Hamcrest "shouldBe" Extension Method

Extension make a number of testing practices simpler and more readable. For example, you can add assertion methods to the type of object you are verifying so that your assertions read more like a sentence:

def <T> shouldBe(T actual, Matcher<T> matcher) {
  if (!matcher.matches(actual)) {
    val excpectedDescription = new StringDescription
    matcher.describeTo(excpectedDescription)
    throw new AssertionError("Assertion Error!\n"+
                             "Expected: " + excpectedDescription+"\n"+
                             "     Got: " + actual)
  }
}

// shouldBe is now available as an extension of any type
(1+1).shouldBe(equalTo(2))
"String".shouldBe(not(instanceOf(Integer::class)))

Operator Overloading


All infix and prefix operators in Xtest are implemented with methods that bind to operators based on naming convention and the best-match argument types. For example the method operator_plus(A, B) implements the A + B infix operator and operator_equals(A, B) impelements the A == B infix operator. A full list of operator to method name mappings can be found in the Xbase documentation.

All default overloaded operators that make Xtest behave like a regular programming language are defined in the Xbase Libraries and available implicitly, without importing them. For example all of the following operators (>, <=, **, ==) are implemented by static methods in IntegerExtensions:

assert 1 > 0
assert 3 <= 3
assert 2**3 == 8

In addition, you can implement and overload operators directly in Xtest

def <T> operator_doubleArrow(T left, T right) {
  assert left == right
}

1+1 => 2         // calls operator_doubleArrow(1+1, 2)
"a ".trim => "a" // calls operator_doubleArrow("a ".trim, "a")

Or you can implement overloaded operators in Java

// this is Java!
public class Operators {
  public static String operator_minus(String a, String b) {
    return a.replace(b, "");
  }
}

And use them in Xtest

// this is Xtest!
import static extension Operators.*
assert "abcde" - "abc" == "de"

Example: Test-specific equality

It is often useful to define test-specific equality when tests have a different notion of equality than production code. It is undesirable to change the equals() method as that causes test code to leak into production. This can be accomplished as shown below by overloading the equals operator in Xtest.

def operator_equals(String left, String right) {
  return left.equalsIgnoreCase(right)
}

assert "aBcDEF" == "abcdef"

Note The equals operator is only overloaded in this Xtest file within the lexical scope of the operator_equals method. If you pass these two strings into a Collection, it will not treat them as equal.

Stack Traces


Stack traces in Xtest look like Java stack traces, except that the Xtest stack trace elements contain the text of the current expression instead of the method name. XtestUtil defines the logic for creating Xtest stack traces.

def reciprocal(int num) { 1 / num }
def int naughtyRecursiveMethod(int arg) {
  reciprocal(arg) + naughtyRecursiveMethod(arg-1)
}
naughtyRecursiveMethod(3)
Error!java.lang.ArithmeticException: / by zero
StackTraces."1 / num"(StackTraces:1)
StackTraces."reciprocal(arg)"(StackTraces:3)
StackTraces."naughtyRecursiveMethod(arg-1)"(StackTraces:3)
StackTraces."naughtyRecursiveMethod(arg-1)"(StackTraces:3)
StackTraces."naughtyRecursiveMethod(arg-1)"(StackTraces:3)
StackTraces."naughtyRecursiveMethod(3)"(StackTraces:5)

Stack traces can jump back and forth between Java and Xtest when an Xtest lambda expression is passed into Java code:

def reciprocal(int num) { 1 / num }
val list = newArrayList(1, 2, 3, 0, 4)
list.sort[a, b|reciprocal(a).compareTo(reciprocal(b))]
Error!java.lang.ArithmeticException: / by zero
StackTraces."1 / num"(StackTraces:1)
StackTraces."reciprocal(b)"(StackTraces:3)
$Proxy211.compare(Unknown Source)
java.util.Arrays.mergeSort(Arrays.java:1270)
java.util.Arrays.sort(Arrays.java:1210)
java.util.Collections.sort(Collections.java:159)
StackTraces."list.sort[a, b|reciprocal(a).compareTo(reciprocal(b))]"(StackTraces:3)

Private Member Access


A small tweak to XtestVisibilityService allows private and protected members of Java classes and objects to be accessed and modified within Xtest. Think of a tool for testing a circuit board that can probe voltages at internal nodes and insert erroneous states to see how the circuitry will respond. Xtest allows you to do the same without needing to change the exposed API of the Java code being tested.

For example, say you had a simple counter that counts up to Integer.MAX_VALUE implemented in Java:

// This is Java!
public class Counter {
  private int count = 0;
  public int increment() {
    if (count < Integer.MAX_VALUE) {
      count++;
    }
    return count;
  }
}

Now you want to write a test that verifies the correct behavior at Integer.MAX_VALUE. To do this in Java without invoking increment() Integer.MAX_VALUE times you would need to expose more through the exported API of Counter.

In Xtest, however, you can do this simply by modifying the internal state of Counter:

// This is Xtest!
val counter = new Counter
counter.count = Integer::MAX_VALUE
assert counter.increment == Integer::MAX_VALUE

If increment() and count were declared static, you could do the same except you need to use the special := static assignment operator:

// This is Xtest!
Counter::count := Integer::MAX_VALUE
assert Counter::increment == Integer::MAX_VALUE

Warning With great power comes great responsibility! For new code it is always best to design for testability. When source code is under your control it is almost always better to refactor it to make it testable. But when working with legacy code or third party libraries that will not change, this may be a good weapon-of-last-resort to have in your arsenal.

Unexecuted Test Code


By default, Xtest marks unexecuted expressions as warnings. This can help you identify unneccessary test code:

def shouldBeSimple(Object input) {
  switch (input) {
    String:  assert input == input.toLowerCase
    Integer: assert input > 0 && input < 10
    List:    assert input.isEmpty
  }
}

1.shouldBeSimple
"a".shouldBeSimple

And it provides a bit more information at-a-glance about test failures when they occur:

xtest "list contains key players" {
  assert list.contains("Mike")
  assert list.contains("Janice")
  assert list.contains("Graham")
  assert list.contains("Lauren")
}

This can be disabled at any time by changing the "mark unexecuted" setting on a global or per-file basis.

Xbase Features


If you have read through the example Xtest snippets above, you have probably noticed that Xtest code is very similar to Java, but there are a few things that look different. That is because Xtest extends from a template language provided by the Xtext team called Xbase. Xbase provides the expression framework (if, else, while, do, etc.), editor support, and Java-interoperability that mimic the "Java experience."

All that Xtest does is add the assert and xtest expressions that are specific to unit testing, the def keyword for defining methods, and import expressions.

This section seeks to provide an overview of what users familiar with Java need to know about Xbase so that they can become effective users of Xtest. For detailed documentation, please refer to the Xbase Language Reference.

Xtest is a sibling of Xtend (also inherits from Xbase) and an uncle of Jnario (inherits from Xtend), so their documentation may be useful as well.

The grammar specifications for Xbase and Xtest are also available for your reference.

Semicolons are optional

Xbase can differentiate one expression from the next on its own so semicolons are optional:

assert 1+1 == 2
assert 7*6 == 42

Variable Declarations and Type Inference

Use val to declare immutable local variables and var to declare mutable local variables. Type inference infers the type of the variable from the right hand side of the assignment.

For example these variable declarations in Xbase:

// This is Xbase!
var mutable = ""
val immutable = 1

Are equivalent to these variable declarations in Java:

// This is Java!
String mutable = "";
final Integer immutable = 1;

Type inference can be used anywhere an expression assigns a value to a variable. For example, a loop iterating through a list of strings does not need to declare the type of the for loop parameter:

for (entry : newArrayList("one", "two", "three")) {
  assert entry.length < 5
}

Sometimes, however the type of a variable cannot be determined statically so type inference may need a little help.

val List<String> listOfStrings = newArrayList

Literals

For the most part, Xbase allows you to declare literals like Java. However a few additional options are available for strings and numbers.

Strings are declared surrounded by single or double quotes. Strings declared with single quotes can contained un-escaped double quotes and vice-versa. Strings may also contain line-breaks:

assert "hello" == 'hello'
assert " \" " == ' " '
assert ' \' ' == " ' "
assert "A\nB" == "A
B"

Numbers can be declared like in Java. In addition BigDecimals and BigIntegers can also be created by appending 'bd' or 'bi' to the number - and BigDecimals and BigIntegers can also be used in mathematical operations:

assert 0xff == 255
assert 0xCAFEBABE#L == 3405691582L
assert 11d == 1.1e1
assert 1367bi.isProbablePrime(10)
assert 1.1bd.scaleByPowerOfTen(-10) == 1.1e-10bd

// finally we can use operators with BigDecimals and BigIntegers!
assert 1.1bd + 2.3bd == 3.4bd
assert 1e100bi % 7bi == 4bi

Collection Literals

CollectionLiterals in the Xbase Libraries defines a number of static methods that are available anywhere in an Xtest file. These static methods allow you to concisely declare new lists, maps, and sets with default contents:

assert emptySet().isEmpty
assert newArrayList(1, 2, 3).size == 3
assert newLinkedHashSet("a", "b").head == "a"
assert newHashMap(1->"one", 2->"two").get(1) == "one"

The -> operator has be overloaded to return a Pair that the Map creation methods take to pre-load key/value pairs into the map.

Everything is an Expression, Everything is an Object

In Xbase, everything is an expression that returns an object and there are no statements.

Primitives are auto-boxed into their associated wrapper objects:

assert 1.toString() == "1"

Block expressions { ... } return the value from the last expression evaluated inside the block:

assert {
  val numerator = 1 + Math::sqrt(5)
  val denomimator = 2
  numerator / denomimator
}.startsWith("1.618")

Branching expressions return the value from branch that is executed:

assert switch "2" {
  case "1": "One"
  case "2": "Two"
} == "Two"

Java conditional functionality can be implemented with the standard if/else expression:

val abs = if (input < 0) -input else input

And exception catching can be used succinctly to provide default values in exceptional cases:

val asNum = try {
  Integer::parseInt(input)
} catch (NumberFormatException e) {
  0
}

Expressions with no return values have void type and return null

// type is void, expression returns "null"
while (iter.hasNext()) sum = sum + iter.next

Static Member Access

To access static members of a Java class you need to use :: instead of a period. You also need to use :: when fully-qualifying the name of a type that you are accessing a static member of. For example:

java::lang::System::out.prinltln("Hello World!")

Accesses the static member out of the fully-qualified System class then invokes the instance method println on the out object.

Type Casts

You can cast an object to another type in Xbase using the as keyword instead of parenthesis, so your type cast reads like a sentence. For example the following snippet casts arg to a Number:

val asNumber = arg as Number

For Loops and the Up To Operator

Similar to Ruby, you can define a numeric range by using the .. operator and for loops can iterate over these ranges:

for (i : 1..10) {
  assert i > 0 && i < 11
}

Enhanced Switch Expression

The Xtext team made a number of improvements to Java's switch expression when they implemented the switch expression in Xbase.

The case-guard can take on object to match against, or a boolean expression:

switch inputInt {
  case 0: "zero"
  case inputInt < 0: "negative"
  case inputInt > 0: "positive"
}

The type-guard can be used instead of or in addition to the case-guard. The type-guard not only helps choos which case to execute based on the type of the input, it also infers the type of the input variable inside that case:

switch obj {
  String: obj.toUpperCase
  Integer: obj.doubleValue
  List<?> case obj.isEmpty: "a list"
}

Notice there are no break statements. Xbase switch case expressions cannot fall-through and therefore break is unneccessary.

Sugared Getters and Setters

Xbase allows you to access simple getter/setter methods as if they were declared as settable properties:

val name = object.name // calls object.getName()
obj.name = name.trim   // calls object.setName(...)

Note Since Xtest allows access to internal variables of Java objects, the sugared getter/setter method may be over-shadowed by access to the actual underlying private member. You can always call the getter/setter explicitly.

Implicit Receivers

Whenever you assign a variable to the reserved keyword it, that object becomes the "implicit receiver" and methods can be called on it without a receiver.

This can be especially useful for testing when you want to set up the state of an object:

def setup(List it) {
  clear  // calls it.clear()
  add(4) // calls it.add(4)
}

Or when you make many assertions about its state:

val it = list
assert size == 0
assert get(0) throws Exception

The with operator

The with => [ ... ] operator is a shortcut that makes the object returned by the left-hand side of the operator the implicit receiver inside the bracketed expression on the right hand side. This allows succint builders for hierarchical data structures:

val tree = newTree("Parent") => [
  children += newTree("Child 1")
  children += newTree("Child 2") => [
    children += newTree("Grandchild 1")
  ]
]

And is handy for asserting many things about an object:

tree => [
  assert description == "Parent"
  assert children.size == 2
  children.get(0) => [
    assert description == "Child 1"
    assert children.empty
  ]
  // ...
]

Lambda Expressions

Xbase supports lambda expressions using brackets as a more readable alternative to anonymous classes in Java.

Lambda expressions can be assigned to a variable:

val lambda = [String s | s.toUpperCase]

This defaults to a Function1 object since it has one argument, with special shorthand notation in Xbase and a single apply method:

val (String)=>String lambda = [String s | s.toUpperCase]
assert lambda.apply("a") == "A"

A lambda expression can also be coerced to any type that has one declared method, and the argument types are inferred by the destination type:

val Comparator<String> comparator = [a, b|
  a.toLowerCase.compareTo(b.toLowerCase)
]

Lambda expressions can be passed into any method. When the last argument of a method takes a lambda-expression-candidate type, you can declare the lambda expression inline, after the closing parenthesis:

// create(String, (String)=>Integer)
create("name") [a|a.length]

Or omit the parenthesis entirely if it is the only argument:

list.sort[a,b|b.compareTo(a)]

When there is only one argument, the inline lambda expression can omit the argument and pipe. The variable is assigned to the it keyword by default:

list.sortBy[item|item.length]
list.sortBy[it.length]
list.sortBy[length]

Xbase provides many extension methods that allow you to use lambda expressions on standard Java collections:

list.forEach[assert it != null]
list.filter[!it.nullOrEmpty]
list.map[toLowerCase].reduce[cur,next|cur+", "+next]
map.mapValues[it*2]

And you can write your own lambda-expression-accepting extension method in Xtest:

def shouldChangeBy(()=>Object action, int expectedDelta, ()=>int numGen) {
  val start = numGen.apply
  action.apply
  val actualDelta = numGen.apply - start
  assert actualDelta == expectedDelta
}

[|list.add("element")].shouldChangeBy(1) [|list.size]

Null-Safe Feature Calls

Xbase allows you to call a method using object?.method() as syntactic sugar for the idiom object == null ? null : object.method() in Java. This lets the method call return null instead of throwing a NullPointerException if the receiver is null:

val List<String> input = null
assert input?.get(0) == null

Linking Xtest into JUnit


Xtest files can be invoked by JUnit tests, which can be linked into existing test suites. This section gives a brief tutorial on setting up a plugin project for this purpose and creating and running the JUnit test.

In Eclipse, select File -> New -> Other... and select Plug-in Project in the resulting wizard and click Next.

New plugin project wizard

Give your project a name, in this case "myproject.test" and click Next.

Second page of plugin project wizard

Deselect "This plug-in will make contributions to the UI" and "Generate an Activator..." check boxes and click Finish.

Third page of plugin project wizard

Open META-INF/MANIFEST.MF and under the Dependencies tab, add the following plugin dependencies:

  • org.xtest
  • org.junit4

And add the following package dependencies:

  • org.apache.log4j
  • org.eclipse.xtext.junit.util
Adding package and plugin dependencies to manifest.mf file

Right click on the project and select New -> Other..., and choose Xtest File. In the wizard that pops up name the new Xtest file "Demo" and fill it out with the following contents:

// Demo.xtest
xtest "suite #1" {
  xtest "test #1" {
    assert true
  }
  xtest "test #2" {
    assert true
  }
}

Then create a new Java class called "RunDemo":

// RunDemo.java
import org.junit.runner.RunWith;
import org.xtest.junit.RunsXtest;
import org.xtest.junit.XtestJunitRunner;

@RunWith(XtestJunitRunner.class)
@RunsXtest("src/myproject/test/Demo.xtest")
public class RunDemo {
}

Then right click on RunDemo.java and select Run As -> JUnit Test. The output should look something like this, very similar to the outline view.

JUnit view with Xtest file output

A future version of Xtest should allow you to add an Xtest standalone dependency to any Maven project so that you don't need to create a plug-in project to run Xtest from JUnit. That will allow you to run Xtest outside of Eclipse. Until then, creating a pluing project is the simplest way to pull in the required dependencies.

Anatomy of the Test Runner


Version 0.1 of Xtest Eclipse Plugin ran all Xtest files in a project during the build/validation phase any time any change was made that caused Java files in that project to rebuild. This was fine if your tests ran so fast that you didn't notice them running. However if your tests start take take any noticeable amount of time, then this is undesirable because all file modifications and some workspace actions are put on hold while Eclipse waits for a build to finish.

Xtest 0.2 improves on this by moving test execution out of the build/validation phase and into a separate job that runs after a build completes. This frees up the workspace while tests are running so that you can go back to editing files. Version 0.2 also provides a visual indicator for the status of the tests that have run or are in progress.

Test Selection

When you save a Java file, the Xtest runner performs dependency analysis and only runs the minimal set of Xtest files that depend on that change.

Xtest uses dynamic analysis to determine which Java classes an Xtest file depends on. When an Xtest file runs, it uses a fresh class loader that records all Java classes that are loaded during execution and stores that list persistently. Then when an incremental build occurs, the Xtest runner checks the Java class delta list against the dependency lists of each Xtest file and only schedules tests that loaded the changed class last time they ran. This produces a subset of the dependencies that static analysis would produce, and doesn't get tripped up in the presence of reflection.

Xtest stores the dependency list in a BloomFilter. A bloom filter is a probabilistic data structure that can accept elements and later be queried to ask if it "might contain" that element. There is a small (3% for Xtest) chance of a false positive when the bloom filter responds true to that query but there is no chance of a false negative. In turn, the bloom filter is extremely small and very fast for insert, query, and serialization. This is ideal for test selection because a file may have a long list of dependencies which should not bloat the workspace and will also need to be serialized/deserialized and queried often. A small risk of false positive only means that if a test is not actually affected by a change, there is a 3% chance it will still run - and there is nothing wrong with that.

When you select Project -> Clean... in Eclipse to manually clean and rebuild projects, all Xtest files in that project or that depend on classes in that project are scheduled for re-execution.

Test Ordering

In addition to only running tests affected by a change, the Xtest runner also sorts the tests in the following order:

  1. Failing tests run first, sorted from fastest to slowest
  2. Tests that have never run before run next
  3. Passing tests run last, sorted from fastest to slowest

This provides as much information on the passing/failing state of tests as quickly as possible.

Visual Feedback

Xtest 0.2 introduces a visual status bar that quickly displays the progress and current passing/failing state of running Xtest files. It has a "run" button that you can click to run all tests:

And it also displays progress when saving a file triggers running a subset of all of the tests:

Don't like the Xtest status bar? It can be disabled in Window -> Preferences under Xtest -> Runner by deselecting the "Install visual Xtest runner at startup..." check box. The next time you restart Eclipse, the bar will be gone.

Class Loader Memory Leaks

Xtest helps you discover class loader memory leaks in your code. A class loader memory leak occurs when an instance of some class loaded from a parent class loader holds onto a reference to an instance of a class loaded from a child class loader. If the object from the parent class loader can't be garbage collected, then the child class loader and all of its classes cannot be garbage collected either.

Since all test files run with a fresh class loader within the main Eclipse process, if your code contains any class loader memory leaks, old instances of classes from previous test runs will pile up. If a JVM memory analyzer shows the Eclipse process's number of loaded classes go up after each test run and not go down after a forced GC, you have a leak.

This is the same situation that web servers have. When you redeploy a new version of code into a web server, it uses a new class loader hoping the old class loader and all old classes will be garbage collected. The Tomcat Wiki has a page explaining different kinds of class loader memory leaks and how to address them.

Warning A number of popular open source frameworks have class loader memory leaks, including Google Guice.

Configuration


You can configure the way that Xtest runs your tests on a global and per-file basis. Per-file settings always override the global setting.

The global settings are available under Window -> Preferences. Search for Xtest in the window that pops up

Eclipse settings under Window->Preferences->Xtest

To edit the same settings in a file, insert any combination of the following at the very top of the file:

runWhileEditing: true
runOnSave: true
markUnexecuted: true

// rest of file ...