Code Monkey home page Code Monkey logo

acai's Introduction

Acai

Build Status Coverage Status

Acai makes it easy to write functional tests of your application with JUnit4 and Guice.

Acai makes it simple to:

  • Inject the helper classes you need into tests
  • Start any services needed by your tests
  • Run between-test cleanup of these services
  • Start up multiple services for testing in the right order
  • Create test scoped bindings

Acai is designed for large functional tests of your application. For example it can help with writing tests which start your backend and frontend server in a self-contained mode with their dependencies faked out and then validates some key user scenarios with Webdriver to give you confidence your complete system works correctly. It can also be useful for tests which validate the integration of a small set of components. Note however that for smaller unit-tests we generally recommend you create the class under test manually rather than using Acai.

Installation

Add a dependency on com.google.acai:acai in your build system to fetch Acai automatically from Maven Central. For example, with Maven add the following to your dependencies in pom.xml:

<dependency>
  <groupId>com.google.acai</groupId>
  <artifactId>acai</artifactId>
  <version>1.1</version>
  <scope>test</scope>
</dependency>

See the artifact details on Maven Central for dependency information for other build systems or to simply download the jars.

Using Acai to inject a test

The simplest test using Acai doesn't register any TestingService bindings at all, it just uses Acai to inject a test with a module:

@RunWith(JUnit4.class)
public class SimpleTest {
  @Rule public Acai acai = new Acai(MyTestModule.class);

  @Inject private MyClass foo;

  @Test
  public void checkSomethingWorks() {
    // Use the injected value of foo here
  }

  private static class MyTestModule extends AbstractModule {
    @Override protected void configure() {
      bind(MyClass.class).to(MyClassImpl.class);
    }
  }
}

Using Acai to start services

The real power of Acai comes when your production server is configured with Guice and you create an alternate test module which configures your server with heavyweight dependencies like databases replaced with local in-memory implementations. You could then start this server once for all tests in the suite. For example:

@RunWith(JUnit4.class)
public class ExampleFunctionalTest {
  @Rule public Acai acai = new Acai(MyTestModule.class);

  @Inject private MyServerClient serverClient;

  @Test
  public void checkSomethingWorks() {
    // Call the running server and test some behaviour here.
  }

  private static class MyTestModule extends AbstractModule {
    @Override protected void configure() {
      // Normal Guice modules which configure your
      // server with in-memory versions of backends.
      install(MyServerModule());

      install(TestingServiceModule.forServices(MyServerRunner.class));
    }
  }

  private static class MyServerRunner implements TestingService {
    @Inject private MyServer myServer;

    @BeforeSuite void startServer() {
      myServer.start().awaitStarted();
    }
  }
}

Note that when a module is passed to Acai in a rule any @BeforeSuite methods are only executed once per suite even if the same module is used in multiple Acai rules in multiple different test classes within that suite. This allows tests of the server to be structured into test classes according to the functionality being tested.

Test isolation

When sharing a locally running backend or fake between multiple test cases as above it's often necessary to clear its state between each test in order to isolate tests from one another.

This can be achieved using an @AfterTest method in a TestingService. The following example clears all data in a local database between tests:

@RunWith(JUnit4.class)
public class ExampleFunctionalTest {
  @Rule public Acai acai = new Acai(MyTestModule.class);

  @Inject private MyServerClient serverClient;

  @Test
  public void checkSomethingWorks() {
    // Perform actions which write to the database here.
    // Any state will be cleared by MyFakeDatabaseWiper after each
    // test case.
  }

  private static class MyTestModule extends AbstractModule {
    @Override protected void configure() {
      install(MyFakeDatabaseModule());
      install(TestingServiceModule.forServices(MyFakeDatabaseWiper.class));
    }
  }

  private static class MyFakeDatabaseWiper implements TestingService {
    @Inject private MyFakeDatabse myFakeDatabase;

    @AfterTest void wipeDatabase() {
      myFakeDatabase.wipe();
    }
  }
}

Test scoped bindings

Occasionally you may wish to have one instance of a class per test and inject this instance in multiple places in the object graph. In this case Guice's default instance scope will not do. Fortunately Acai provides a @TestScoped annotation which can be used to achieve exactly this.

For example we may define a module for using Webdriver (a popular browser automation tool) in our tests like so:

class WebdriverModule extends AbstractModule {
  private static final Duration MAX_WAIT = Duration.standardSeconds(5);

  @Override
  protected void configure() {
    install(new TestingServiceModule() {
      @Override protected void configureTestingServices() {
        bindTestingService(WebDriverQuitter.class);
      }
    });
  }

  @Provides
  @TestScoped
  WebDriver provideWebDriver() {
    // Provide the driver here; precisely one instance will be
    // created per test case.
  }

  @Provides
  WebDriverWait provideWait(WebDriver webDriver) {
    return new WebDriverWait(webDriver, MAX_WAIT.getStandardSeconds());
  }

  static class WebDriverQuitter implements TestingService {
    @Inject Provider<WebDriver> webDriver;

    @AfterTest void quitWebDriver() throws Exception {
      // Calling get on the Provider here returns the instance
      // for the test case which we are currently tearing down.
      webDriver.get().quit();
    }
  }
}

One important point to note when using @TestScoped bindings is that TestingService instances are instantiated once for all tests outside of test scope. Therefore if you wish to access @TestScoped bindings in a @BeforeTest or @AfterTest method you should inject a Provider and call get on it within those methods as shown in the above example.

When not to use TestScoped

Note that while @TestScoped works well for helpers injected only into tests (such as the WebDriver instance in the above example) for fakes and other objects which are shared with the system under test it is usually simpler to use a single instance and reset its state with a TestingService (see Test isolation). This technique avoids some of the limitations of @TestScoped such as the fact it can only be injected on the test thread or child threads of the test and makes it possible to inject the instance into objects whose lifetime is longer than that of an individual test.

Services which depend upon each other

If the services you need to start for tests must be started in a specific order you can express this using the @DependsOn annotation.

For example:

@RunWith(JUnit4.class)
public class ExampleFrontendWebdriverTest {
  @Rule public Acai acai = new Acai(MyTestModule.class);

  @Inject private SomeFrontendFeaturePageObject featurePage;

  @Test
  public void checkSomethingWorks() {
    // Test the frontend client using the webdriver page
    // object here.
  }

  private static class MyTestModule extends AbstractModule {
    @Override protected void configure() {
      // Normal Guice modules which configure your
      // server with in-memory versions of servers and
      // a test module configuring a webdriver client.
      install(MyServerModule());
      install(MyFakeDatabaseModule());
      install(WebDriverModule());

      install(new TestingServiceModule() {
        @Override protected void configureTestingServices() {
          bindTestingService(MyFrontendRunner.class);
          bindTestingService(MyBackendRunner.class);
        }
      });
    }
  }

  @DependsOn(MyBackendRunner.class)
  private static class MyFrontendRunner implements TestingService {
    @Inject private MyFrontendServer myFrontendServer;

    @BeforeSuite void startServer() {
      myFrontendServer.start().awaitStarted();
    }
  }

  private static class MyBackendRunner implements TestingService {
    @Inject private MyBackendServer myBackendServer;

    @BeforeSuite void startServer() {
      myBackendServer.start().awaitStarted();
    }
  }
}

In the above example MyFrontendRunner is annotated @DependsOn(MyBackendRunner.class) which will cause Acai to start the backend server before starting the frontend.

API

As shown in the above examples Acai has a relatively small API surface. Firstly, and most importantly, there is the Acai rule class itself which is used as a JUnit4 @Rule and is passed a module class to be used to configure the test.

The module class passed to the Acai constructor may optionally use TestingServiceModule to bind one or more TestingService implementations.

The TestingService interface is purely a marker to allow Acai to know which classes provide testing services. To actually do anything implementations of this interface should add zero argument methods annotated with one of @BeforeSuite, @BeforeTest or @AfterTest. These methods will be run before the suite, before each test or after each test respectively. You may add as many methods annotated with these annotations as you wish to a TestingService; Acai will find and run them all when appropriate.

For more advanced use-cases where instance scope is not sufficient the @TestScoped annotation can be used to create one instance of a class per test case.

Finally a TestingService implementation can be annotated @DependsOn to signal its @BeforeSuite and @BeforeTest methods need to be run after those of another TestingService. This provides a simple declarative mechanism to order service startup in tests.

Refer to the examples above to see the API in action.

Contributing

We'd love to accept your patches and contributions to this project. There are a just a few small guidelines you need to follow. See the CONTRIBUTING.md file for more information.

Disclaimer

This is not an official Google product.

acai's People

Contributors

icmdaf avatar nlativy avatar poletti-marco avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

acai's Issues

Use standard JUnit4 annotations.

Consider if we can kill the custom annotations in favour of using @Before, @After and @BeforeClass from JUnit4. Would this make sense and would the semantics be clear?

Test scope left open if test injection fails

We enter and exit test scope in TestEnvironment#beforeTest and TestEnvironment#afterTest respectively. The code in Acai which calls these methods looks like:

TestEnvironment testEnvironment = getOrCreateTestEnvironment(module);
testEnvironment.beforeSuiteIfNotAlreadyRun();
testEnvironment.beforeTest();
testEnvironment.inject(target);
try {
  statement.evaluate();
} finally {
  testEnvironment.afterTest();
}

If testEnvironment.inject(target) throws an exception we leave the scope open and all future tests fail with the exception:

java.lang.IllegalStateException: TestScope is already in progress.
    at com.google.common.base.Preconditions.checkState(Preconditions.java:177)
    at com.google.acai.TestScope.enter(TestScope.java:37)
    at com.google.acai.Acai$TestEnvironment.beforeTest(Acai.java:160)
    at com.google.acai.Acai$1.evaluate(Acai.java:78)

This also means, of course, that we fail to do any per-test tear-down as well in this case.

In fixing this issue another case to be aware of is that if a user defined @BeforeTest method fails currently this would also cause the TestScope to not be exited so simply moving testEnvironment.inject(target) into the try block will not make the code completely robust. We probably need to decouple the enter/exit scope from running @BeforeTest and @AfterTest methods.

Support running TestingServices in parallel

Tests that start multiple servers using @BeforeSuite in multiple testing services may be slower than they need to be as these @BeforeSuite methods are run sequentially.

It might be nice to support running these methods in parallel. I think if we do this it should probably be the only mode of operation to avoid adding too many options to the API. This may require updates to existing clients to add missing @DependsOn annotations.

Alternatively we could support an async version of @BeforeSuite etc that perhaps returns a future which we wait on before starting the tests. That way we don't have an on/off control in the API but clients don't start getting their code unexpectedly running in parallel automatically.

Support injecting methods of testing services

Currently each TestingService is instantiated once per environment. This means if you wish to do something with an object bound with a @TestScoped binding you need to use a Provider.

A common example might be a service that quits the webdriver instance started for an individual test:

class WebDriverQuitter implements TestingService {
  @Inject Provider<WebDriver> webDriver;

  @AfterTest void quitWebDriver() throws Exception {
    // Calling get on the Provider here returns the instance
    // for the test case which we are currently tearing down.
    webDriver.get().quit();
  }
}

If we supported injecting each @BeforeTest, @AfterTest method then this could become:

class WebDriverQuitter implements TestingService {
  @AfterTest void quitWebDriver(Webdriver webDriver) throws Exception {
    webDriver.quit();
  }
}

injecting the correct instance for the current test scope.

While cute this adds a bit of complexity so we should consider if it's worth bloating the API for something that's already achievable anyway. Also the scope will not be test scope when running @BeforeSuite methods so we should be careful that doesn't cause confusion.

Also would users then want to inject test methods too? That doesn't add as much value since the scope is the same as when injecting the test object. It would mean objects used in one test case only could be injected there rather than being fields but in such cases maybe the user should be splitting into multiple test classes anyway. Worth considering if this inconsistency between test methods and test services would be confusing though if we only supported injecting the service methods.

TestScoped doesn't work in non-test thread.

When start a thread from the test, it can't inject @TestScoped instance to the new thread.
It throws com.google.inject.OutOfScopeException: Attempt to inject @TestScoped binding outside test:

Provide a clean way to stop services

Currently TestingService classes can provide a @BeforeSuite method but there is no corresponding @AfterSuite. This means if you start a bunch of services in the @BeforeSuite method there is no easy way to stop them when you are done testing.

One work around is to attach a JVM shutdown hook in your @BeforeSuite method but this is a bit ugly.

RunListener#testRunFinished in JUnit is basically what we need but a RunListener can only be attached via JUnitCore which would mean forcing users to implement their own main method or use one we provide. That seems much too intrusive.

The proposal in junit-team/junit4#874 may be useful in implementing this but we would need something that could attach the listener while still only requiring the user to add one thing to their tests. Telling people to add the Acai @Rule and also to register a listener for things to work correctly would be too onerous. Probably Acai could be implemented as a listener rather than a rule.

Fail on misuse of TestScoped bindings

A user reported an issue where they were binding an instance in test scope and then injecting that into a singleton scoped object. Apparently this does not produce an error from Acai but leads to incorrect behavior in the tests.

If possible it would be nice to fail fast in such a scenario.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.