My favorites | Sign in
Project Home Downloads Wiki Issues Source
Search
for
ProposalForTestingApi  
Writing and running tests in Noop
proposal
Updated Nov 13, 2009 by aeagle22206

Introduction

While other languages leave testing to third-party library authors, Noop builds in testing from the language syntax onward. Although test constructs are available in the language, third-party testing libraries are still supported and encouraged, so we intend to leave the door open for alternate test runners, assertion libraries, mocking libraries, etc.

We believe that test code looks different from production code. Tests should be very simple, with minimal conditional logic, so that they may be read as documentation. Each test should read like a little story of an object's life and interactions. We don't want to have type hierarchies, subtle behaviour, or lots of helper methods in a test.

Some awkward things happen when you add testing as a library in a language:

  • Associating a test with a class requires a naming convention, and only works for unit tests which map 1-1 with production classes
  • Refactoring tools don't know to rename the test when you rename a class, breaking that naming convention
  • must have a separate src-test root so tools know which sources to package for production, and be sure production sources don't depend on test sources
  • tests are methods with a special naming convention, and the condition being tested must be encoded into a valid method name, ie testReaderShouldFindTheEndOfLine
  • tests are grouped into classes, but any higher-level grouping requires a test suite class which lists the test classes to include
  • test runner doesn't easily know how to run a subset of tests within a test class
  • don't have any special syntax we can use in test code

The test keyword

A test is an entity, distinct from a class or method whose purpose is to run tests. It may be declared in a file with any name, which may live under the src-test source root of the codebase, or in the same source root with production sources. It is given a string name, meaning that the usual restrictions on identifier names don't apply. That is then followed by the test block.

// In src/test/noop/mypackage/MyImportantTests.noop
test "Look, ma! I'm a valid test name!" { ... }

Inside the test block, you can construct fixtures, execute code, and assert on the results. We provide a DSL for assertions, based on Hamcrest matchers.

test "try again" {
  -1.abs() should equal(1);
}

Here, the should keyword is a special operator, which evaluates the left-hand-side, then invokes the right-hand-side expression using a Hamcrest matcher.

Users may choose to use another test framework if they wish, by whatever mechanism that test framework uses. However, since Noop doesn't allow static methods, some of these API's will be hard to use. We will probably need to provide a way to call static methods on "legacy" Java code at some point.

Grouping tests into suites

This is a common feature of test frameworks. In most, tests are methods, which are grouped into a testcase, which is a class. Any higher-level grouping is done with a separate Suite class, or by discovery via metadata like an annotation.

In Noop, a suite is just another test block enclosing some tests, and may be arbitrarily nested, and divided among several files.

// In FastDateTests.noop
test "really fast tests" {
  test "fast date tests" {
    test "date can be added" {
      Date("2009-10-11") + 1.day() should equal(Date("2009-10-12"));
    }
  }
}
// In FastIntTests.noop
test "really fast tests" {
  test "some int tests" {
    test "my int tests" {
      test "thing" { 1.plus(2) should equal(3); }
    }
  }
}

In this example, the resulting structure is

- Suite "really fast tests"
- Suite "fast date tests"
- Test "date can be added"
- Suite "some int tests"
- Suite "my int tests"
- Test "thing"

Alternative: use tags instead of suites

The problem with suites is that they statically define which groups of tests may be run together. If you have understandable tagging on tests, it may be easier to describe the group of tests you wish to run using tags, rather than having to use one of the provided suites. This allows the flexibility of a many-to-many mapping from tags to tests, which you can't have with suites.

Replacing dependencies in tests

This is what Noop is all about! To replace a dependency, use the scope keyword to create a child injector, and bind a different thing to the type you want to replace. For example,

// In MyClass.noop
class MyClass(Console c) {
  Void printThing() {
    c.println("thing");
  }
}
// In ATest.noop
test "Should print thing" {
  FakeConsole c = FakeConsole();
  scope (Console -> c) {
    MyClass(c).printThing();
  }
  c.getPrintedText() should equal("thing");
}

As this test runs, the Console type is bound to our FakeConsole instance, and within that scope, we create the class-under-test. The injector will provide our FakeConsole to satisfy the MyClass dependency, so we will be able to make assertions about what is printed.

The unittest keyword

A unittest is a special test where the fixture is assumed to be a single class. This allows the test code to refer to the class-under-test as "this", and avoid creating the fixture explicitly.

For this to work, the unittest may not be a root of the testsuite tree. It must appear beneath a test suite that defines a single type to be tested, or within the class itself (see the next section for details).

test "I'm testing MyClass again" scope (ClassUnderTest -> MyClass) {
  unittest "should print thing again" scope (Console -> FakeConsole) {
    printThing();
    c.asInstance(FakeConsole).getPrintedText() should equal("thing");
  }
}

Writing unit-tests inline

Since unittests are special and apply to a single class, they are allowed to appear inside a production source file. This allows them to act as a true "insider", with access to anything defined in the class, including private properties.

Here's an example:

import noop.Console;
import noop.FakeConsole;

class HelloWorld(Console c) {
  Int main(List args) {
    c.println("Hello tests!");
    return 1;
  }
  
  test "prints hello" scope (Console -> FakeConsole) {
    main(List()) should equal(1);
    c.asInstance(FakeConsole).getPrintedText() should contain("Hello tests!"));
  }
}

Notes:

  • The import noop.FakeConsole is allowed, and when running tests, this class will be available on the classpath. When running production code, the class is not available, but the import should be unused. If code outside the test block uses the FakeConsole, it will cause a runtime exception.
  • The cast of the Console to FakeConsole is annoying, but a necessary result of living inside the HelloWorld class where the c property is of type Console.

How tests are handled by language tools

In the interpreter, the test block is retained as a special construct, and executed when testing.

In the compiler, the behavior is governed by a flag, similar to the "debug" flag to many compilers. When the compiler is run in "production" mode, the test block is stripped from the emitted code. In "test" mode, it is retained, and converted to a normal class, or if nested, to a method in the host class.

Note that when tests appear in production code, the production code can be compiled independently of the tests, as long as you are not compiling in "test" mode - we lazy evaluate the imports and toss the test blocks. But, when you compile in test mode, both the production and test sources must be provided to the compiler in one step. Fortunately, our compiler knows about testing, and will allow a separate testSourceRoots parameter to declare where the tests are.

When the tests are compiled into classes and methods, we will need an appropriate strategy to name the class or method. If the target bytecode language doesn't allow spaces and other characters that we allow in test names, we'll have to escape them. We would still like to retain the original test name as metadata on the class or method, to display later.

Behavioral Mocks

Libraries like JMock are really convenient for providing an instance of a dependency that can assert how it is used. For example, a MockDatabase would be able to say if the correct thing was inserted.

One way to do this...

Every test block allows expectations to be set, and mocks to be created, by delegating the test class to a !JMock Mockery, and asserting that the mockery is satisfied

test "a record is inserted into the database" {
  Database d = createMock(Database);
  checking(Expectations() {
    oneOf(d).insert("record"); will(returnValue("OK"));
  });
  MyClass(d).run();
}

TODO

Look at AtUnit http://code.google.com/p/atunit/ and Guiceberry http://code.google.com/p/guiceberry/ for good ideas

Robertsdionne's Mock Syntax Brainstorming

First, I'll assume we declare our methods like this (see Possible Method Syntax Twelve on ProposalForMultipleReturnValues):

class Foo() {

  doSomething(Int x, Int y) returns (Int a, Int b) {
    ...
  }
}

I'll also assume we have something akin to function pointers:

Foo foo = ...;

(Int, Int) returns (Int, Int) func = foo.doSomething;

(Int a, Int b) = func(5, 6); // just like calling foo.doSomething(5, 6);

And that we have closures, like this:

(Int x, Int y) returns (Int a, Int b) func = {
   ...
};

(Int a, Int b) = func(5, 6); // now has nothing to do with foo.doSomething

And now we get to expectation and mock behavior syntax:

class MyClass(Foo foo) {

  behave(Int x, Int y) returns (Int) {
    (Int a, Int b) = foo.doSomething(x, y);
    return a * b;
  }
}

test "Test MyClass" {
  test "behave(x, y) returns doSomething(x,y).a * doSomething(x,y).b" {
    Foo foo = Foo.mock();
    MyClass myClass = MyClass.new(foo);

    foo.doSomething(null, null) returns (5, 6); // no expectation, just stubbed behavior

    assertEquals(30, myClass.behave(null, null)); // don't care about inputs
  }

  test "same as above but with expectations" {
    Foo foo = Foo.mock();
    MyClass myClass = MyClass.new(foo);

    expect foo.doSomething(2, 3) returns (55, 10); // this time we expect the call

    assertEquals(550, myClass.behave(2, 3));
  }
}
Comment by project member toc...@gmail.com, Aug 15, 2009

I would argue that having a class name for a test is useful because if the test fail having something like:

UnderTestTest?.test "The computation should always return 10"

helps me find out which file/test class I need to go check to read the test code.

Comment by project member christia...@gmail.com, Aug 16, 2009

I'd like to recommend that we, by default, undermine any access to sockets, files, or other externalities in the default test(){} invocation. This would make the default test an isolated unit test, at least as far as external system elements go. It's still not forcing absolute isolation of a component, but provides a certain minimal constraint on unit tests.

A keyword or modifier would reduce these restrictions for execution of integration tests launched from the xunit-like integrated test runner. Mostly I just want the ability to discourage evil testing, by making the developer explicitly choose to override the restrictions.

Comment by project member robertsd...@gmail.com, Aug 18, 2009

Mocked constructor parameters should already be available to the developer within the context of the testcase for any class he is testing. This would also eliminate default access to IO-intensive classes. For instance, a class that takes a URL and a URLConnection as its constructor parameters would automatically receive a mock URL and a mock URLConnection within the context of the testcase.

Comment by project member christia...@gmail.com, Aug 19, 2009

I'm not sure you can reasonably provide mocks in a transparent and automatic way, because a Mock provides a conversation between the dependent and dependency. That conversation has meaning in terms of the test, so unless you can find a way to declare that interaction, it's not so helpful to randomly get a proxied dependency. Or maybe I'm misunderstanding your intent. can you do up a little code sample to show what you mean?

Comment by project member robertsd...@gmail.com, Sep 10, 2009

Christian,

Imagine I have the following class I would like to unit test:

// LineItem.noop

class LineItem(readable Purchasable product, readable Money basePrice, readable writable Discount discount, Calendar calendar) {
  readable virtual Money finalPrice {
    get {
      return discount.apply(basePrice, calendar.now());
    }
  }
}

I've rewritten the definition of LineItem? from NullObjectPatternProposal so that the properties are shown above as class dependencies.

Here is what I had in mind for unit tests that provide the developer with mocked class dependencies:

// LineItemTest.noop

test {

  test(LineItem, "Calculate the final price of a LineItem given a Discount.",
      Purchasable product, Money basePrice, Discount discount, Calendar calendar) {
    // product, basePrice, discount and calendar are all mock objects already

    // replace basePrice with a real Money object
    basePrice = Money.new("$", 500, 00); // $500.00

    LineItem underTest = LineItem.new(product, basePrice, discount, calendar);

    expect calendar.now() and return new Date(5, 5, 2009, 10, 36, 00) as theDate;
    expect discount.apply(basePrice, theDate) and return basePrice * 0.5 as expectedFinalPrice;

    replay; // my brain is tainted by EasyMock

    assertEquals(expectedFinalPrice, underTest.finalPrice);

    verify;
  }

  test(LineItem, "Calculate the final price of a LineItem given a null Discount.",
      Purchasable product, Money basePrice, Discount discount, Calendar calendar) {
    basePrice = Money.new("$", 500, 00); // $500.00

    LineItem underTest = LineItem.new(product, basePrice, null, calendar); // notice null is passed in for the Discount parameter

    replay;

    assertEquals(basePrice, underTest.finalPrice);

    verify;
  }
}
Comment by project member christia...@gmail.com, Sep 10, 2009

But where's the mock object? You're describing a non-behavioural fake, which is fine - in fact null-references as no-op implementations is great for creating boilerplate-light fakes. But mocks describe expected behaviour (calls to their API in the order of calling) so they're not just creatable in this way. You have to set expectations.

Maybe this is just a matter of using the term "mock" too loosely as I've seen elsewhere.

Comment by project member robertsd...@gmail.com, Sep 10, 2009

Here's an alternative, using "Guice-style" syntax, that provides mocked dependencies by default:

test(LineItem) {

  configure {
    // Configure your bindings a la Guice.  In essence, this unit test
    // is its own Guice-style Module, with all dependencies of LineItem
    // pre-bound to mock implementations.

    // Supply your own implementations here:
    bind (Money basePrice) to instance Money.new("$", 500, 00); // "Money basePrice" is analogous to a Guice "Key": a type-annotation pair
  }

  test("Calculate the final price of a LineItem given a 50% Discount.") {
    inject LineItem underTest; // Mr. Injector, give me an instance of LineItem with all its dependencies provided automagically

    // Mr. Injector, give me references to my mock objects so I can declare expectations
    inject Calendar calendar;
    inject Discount discount;
    inject Money basePrice;

    expect calendar.now() and return new Date(5, 5, 2009, 10, 36, 00) as theDate;
    expect discount.apply(basePrice, theDate) and return basePrice * 0.5 as expectedFinalPrice;

    replay; // Again, we shouldn't religiously adhere to EasyMock syntax, but I'm most familiar with it.

    assertEquals(expectedFinalPrice, lineItem.finalPrice);

    verify; // verify mocks behaved themselves.
  }

  test("Calculate the final price of a LineItem given a null Discount.") {
    // Sometimes a single test needs more refined dependencies. We may further revise the Guice-style Module bindings here
    configure {
      bind (Discount discount) to null;
    }

    inject LineItem underTest; // Mr. Injector, give me an instance of LineItem with all its dependencies provided automagically

    // Mr. Injector, give me references to my mock objects so I can declare expectations
    inject Calendar calendar;
    inject Discount discount;  assertNull(discount); // Just to demonstrate how the above "configure {}" block changed the injector
    inject Money basePrice;

    replay; // Again, we shouldn't religiously adhere to EasyMock syntax, but I'm most familiar with it.

    assertEquals(basePrice, lineItem.finalPrice);

    verify; // verify mocks behaved themselves.
  }
}
Comment by project member robertsd...@gmail.com, Sep 10, 2009

Christian,

I did define expectations, just after the fact, a la EasyMock??:

expect calendar.now() and return new Date(5, 5, 2009, 10, 36, 00) as theDate;
expect discount.apply(basePrice, theDate) and return basePrice 0.5 as expectedFinalPrice;
Comment by project member christia...@gmail.com, Sep 10, 2009

I see what you're saying. If we can work out how we interact with the injector/container, then it may just be that a test is its own scope and this could work out well.

Comment by project member christia...@gmail.com, Sep 10, 2009

Oh - crap, sorry Bobby. I missed it. I was looking at the second test in that example. My bad.

Comment by cjdo...@gmail.com, Sep 17, 2009

What about data-driven tests? TestNG's @DataProvider? support has been enormously useful to my team by quickly creating lots of tests for branchy functions that transform input to output.

Comment by ken.ch...@gmail.com, Sep 17, 2009

Although EasyMock? is an excellent mock object framework, it is a liitle too verbose. Perhaps noop could use Mockito syntax (no replay() or verify(), and the error messages are easier to grok.

Comment by Arnold.B...@gmail.com, Sep 22, 2009

You know, if our Noop method signatures were stronger (eg. specifying that a parameter must be non-null, or within a range (0-100), etc) we would have less need for some of our testcases and we can focus on more interesting business tests.

public void setPartyIdentifier(final Integer partyId NotNull Range(1-999999))
{
}
Comment by jakub.si...@gmail.com, Oct 26, 2009

Hey. I would like to add that comment from Arnold Barber is a good point. Maybe as we (we? hmm, sounds like a commitment...) want to give better support for testability and maintainability, 'just' integrating xUnit and Mock frameworks is not that big improvement? Pair class concept is something new, but that I would like to see something more. As methods signatures to be stronger, maybe all class invariants concept could be integrated (there is JavaModelingLanguage anyway). And to support that something that does lot's of test in the background, something like T2Framework. This way you have hard specification in the code, lots of boundary values and other generic tests done in the background. Thanks to that you have more time for focus on TDD instead of trivial method testing, or more time to do some serious testing instead of 2-3 test cases that prove nothing. This would make testing integration really stand out.


Sign in to add a comment
Powered by Google Project Hosting