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.
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.
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.
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 }
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.
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.
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.
Now all parts of Eclipse have updated to show that the test is failing.
Here is a taste of what the Eclipse editor can look like in a more complex situation:
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 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 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
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
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
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>
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.
val i = 0;
val num = 1/i
java.lang.ArithmeticException: / by zero Exceptions."1/i"(Exceptions:2)
You can use import
and import static
in
Xtest just like in Java to import any third party assertion libraries (or code under test).
import static org.junit.Assert.*
val actual = "abc"
val expected = "abc123"
assertEquals("strings", expected, actual)
org.junit.ComparisonFailure: strings expected:<abc[123]> but was:<abc[]> org.junit.Assert.assertEquals(Assert.java:125) Exceptions."assertEquals("strings", expected, actual)"(Exceptions:5)
import static org.junit.Assert.*
import static org.hamcrest.CoreMatchers.*
assertThat("abc", is(not(instanceOf(String::class))))
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)
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")
java.lang.AssertionError Exceptions."mockList.add("a")"(Exceptions:7)
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 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"
Test cases can be nested to any depth, which is convenient for grouping test cases into suites:
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.
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.
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!" }
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.
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.
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!"
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.
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 // ...
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) } }
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 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
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)))
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"
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 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)
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))]
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)
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.
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.
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.
Xbase can differentiate one expression from the next on its own so semicolons are optional:
assert 1+1 == 2 assert 7*6 == 42
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
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
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.
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
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.
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
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 }
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.
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.
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 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 ] // ... ]
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]
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
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.
Give your project a name, in this case "myproject.test" and click Next.
Deselect "This plug-in will make contributions to the UI" and "Generate an Activator..." check boxes and click Finish.
Open META-INF/MANIFEST.MF and under the Dependencies tab, add the following plugin dependencies:
And add the following package dependencies:
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.
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.
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.
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.
In addition to only running tests affected by a change, the Xtest runner also sorts the tests in the following order:
This provides as much information on the passing/failing state of tests as quickly as possible.
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.
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.
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
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 ...