Introduction

Over the years I have written and collected various approaches to unit testing challenging code. This library is my attempt to formalize these tools into a cohesive codebase and provide them for others to use.

Test Things? Is it a statement, as in "things for testing", or is it a command, telling you to "go forth and test"? It’s up to you.

Warning
This is project is considered a BETA release at this point. I don’t foresee any breaking changes at this point, but keep in mind that until a 1.0.0 release things are subject to change.

Getting Started

The Test-Things artifacts are available via the Maven Central repository. Below are the dependency coordinates for Gradle and Maven.

For Gradle:

testImplementation 'io.github.cjstehno:test-things:0.1.0'

For Maven:

<dependency>
    <groupId>io.github.cjstehno</groupId>
    <artifactId>test-things</artifactId>
    <version>0.1.0</version>
    <scope>test</scope>
</dependency>

Data Fixtures

In testing, I often find myself just needing a handful of values for test fixtures. The test-things library provides a handful of useful sources for "grouped" fixture values:

Color Names. The ColorName enum has various color names.

Female Names. The FelameName enum contains the most common female names, per some list I found on the internet.

Male Names. The MaleName enum contains the most common male names, per some list I found online.

Unisex Names. The UnisexName enum contains the most common unisex names, again, found on some web site.

Birth Genders. The BirthGenders enum of the birth genders.

Phonetic Alphabet. The PhoneticAlphabet enum has the military phonetic alphabet - one of my go-tos.

Planets. The Planet enum has the names of all nine planets…​ yes, Pluto I still love you.

US States. The UsState enum has the names of the fifty United States of America.

Person. The Person class is a generic simple person object that is also serializable.

JUnit 5 Extensions

An interesting, and useful part of JUnit 5 is the Extension Mechanism. You can quickly and easily add extensions to make your testing easier and more robust.

The test-things library provides a handful of extensions to aid in test simplification.

  • The LifecycleExtension provides a means of getting some callbacks in before the extensions run.

  • The DatabaseExtension provides a DataSource-based testing environment.

  • The SharedRandomExtension provides a means of pinning the SharedRandom instances for repeatable testing.

  • The ResourcesExtension provides helpers for loading and working with classpath resources in tests.

  • The LogAppenderExtension provides a means of capturing logging events for test inspection.

LifecycleExtension

The LifecycleExtension is an extension used to an occasional gap in the JUnit extension framework - specifically around how the before and after callbacks are applied.

Consider the case when you have an extension configured in a test as follows:

@ExtendWith(HelpfulExtension.class)
class SomeInterestingTest {

    private DataProvider provider;

    @BeforeEach void beforeEach(){
        provider = configureProvider();
        provider.start();
    }

    @AfterEach void afterEach(){
        provider.shutdown();
    }

    // Tests that use the provider with the extension
}

Here we have a (fictional) DataProvider that must be configured in a specific manner for the test; however, this DataProvider is also discovered by the HelpfulExtension. The problem is that the @BeforeEach and @AfterEach annotated methods are executed after those provided by the extension, meaning that depending on how the extension is written, you may not have the provider populated when you need it - if it’s your extension you can modify the extension to resolve this issue, but if the extension is from a 3rd party you need to find another approach.

That’s where the LifecycleExtension comes into play. If you add it before the HelpfulExtension in the list of extensions and then modify the two configuration methods as follows:

@ExtendWith({LifecyleExtension.class, HelpfulExtension.class})
class SomeInterestingTest {

    private DataProvider provider;

    @Lifecycle(BEFORE) void beforeEach(){
        provider = configureProvider();
        provider.start();
    }

    @Lifecycle(AFTER) void afterEach(){
        provider.shutdown();
    }

    // Tests that use the provider with the extension
}

The LifecycleExtension will now execute before the HelpfulExtension. What this extension does is look for the methods annotated with a @Lifecycle annotation, of which there are four types: BEFORE_ALL, BEFORE_EACH, AFTER_EACH, and AFTER_ALL, mapping to the standard JUnit 5 lifecycle callbacks of the same name.

When the test executes, the LifecycleExtension will find all of the lifecycle-annotated methods and execute them in the order they are discovered, then the other extensions will be applied, and then finally the callbacks for the test itself.

In our example, the beforeEach() and afterEach() methods are called early enough to configure the provider instance so that everything works as it should.

Similarly, the extension has support for BEFORE_ALL and AFTER_ALL static lifecycle methods.

Note that the types of method allowed for each lifecycle extension point is as follows:

Lifecycle Point Method modifier

BEFORE_ALL

static

BEFORE_EACH

non-static

AFTER_EACH

non-static

AFTER_ALL

static

Tip
If your lifecycle annotated methods are not being executed, be sure that your method signature meets the criteria described above.

DatabaseExtension

The DatabaseExtension is provided as a framework-agnostic means of setting up and tearing down a database, using a provided DataSource instance.

Before Each

The DataSource used is created or resolved before each test based on the @PrepareDatabase. The @PrepareDatabase annotation defines a creator property, whose value is the name of a method used to create the DataSource. The method must return a DataSource instance. The method used to create the database is resolved in the following order:

  1. If the test method is annotated with the @PrepareDatabase annotation and it has a value set for the creator property, that value will be used as the name of the "creator" method, which will be executed to create the DataSource.

  2. If the test class is annotated with the @PrepareDatabase annotation and it has a value set for the creator property, that value will be used as the name of the "creator" method, and it will be executed to create the DataSource.

  3. If a method exists on the test class with the following signature: DataSource createDataSource(), it will be used to create the `DataSource'.

  4. Lastly, if a field exists with type DataSource, it will be used as the data source - this field must have a value associated with it before the extension before-each callback is run. Consider using the LifecycleExtension to ensure that it is ready when needed.

Before the test method is run, the extension also allows for one or more setup scripts to be executed. The @PrepareDatabase annotation provides a setup property, taking one or more String values. These values will be used as classpath resource paths to load SQL script content. They are resolved in the following manner:

  1. If the test method is annotated with @PrepareDatabase:

    • if the additive property is true (the default) and the setup property is defined:

      • if the test class is annotated with @PrepareDatabase and it has a value for the setup property, it’s scripts will be executed against the DataSource

      • then, the scripts defined in the method annotation will be executed against the DataSource.

    • if the additive property is false and the setup property is defined:

      • each of the values in the setup property will be read and executed on the DataSource.

  2. If the test class is annotated @PrepareDatabase and it has a value for the setup property, those scripts will be executed on the DataSource.

Tip
If you are using a database migration tool like liquibase or flyway, you can still use it to build your schema by running it in your "creator" method and then tearing it down in your "destroyer" method. The "setup" and "teardown" scripts could still be used to populate the database if needed.

After Each

After each test method is run, the extension allows for one or more tear-down scripts to be executed. The @PrepareDatabase annotation provides a teardown property, taking one or more String values. These values will be used as classpath resource paths to load SQL script content. The scripts are resolved in the following manner.

  1. If the test method is annotated with @PrepareDatabase:

    • if the additive property is true (the default) and the teardown property is defined:

      • if the test class is annotated with @PrepareDatabase and it has a value for the teardown property, it’s scripts will be executed against the DataSource

      • then, the scripts defined in the method annotation will be executed against the DataSource.

    • if the additive property is false and the teardown property is defined:

      • each of the values in the teardown property will be read and executed on the DataSource.

  2. If the test class is annotated @PrepareDatabase and it has a value for the teardown property, those scripts will be executed on the DataSource.

The DataSource is "destroyed" using a "Destroyer Method" after each test is executed. Similar to the "Creator Method", the destroyer method is defined by the destroyer property of the @PrepareDatabase annotation. The method must accept a DataSource parameter and is responsible for performing any cleanup or shutdown operations required by the DataSource. It will be resolved in the following order:

  1. If the test method is annotated with the @PrepareDatabase annotation and it has a destroyer property defined, that value will be used as the destroyer method name. It will be executed with the DataSource passed into it.

  2. If the test class is annoated with the @PrepareDatabase annotation and it has a destroyer property defined, that value will be used as the destroyer method name, and it will be executed with the DataSource passed into it.

  3. Lastly, if a method exists on the test class with the following signature: void destroyDataSource(DataSource), it will be executed to perform the destruction handling, giving the DataSource passed to it.

DataSource Parameter

If a test method is given a DataSource argument, it will be populated with the resolved DataSource for use in the test method.

Example

The following is an example with all the bells and whistles - see the unit tests for more scenarios.

@ExtendWith(DatabaseExtension.class) @PrepareDatabase(
    creator="createDs",
    setup={"/db-create.sql", "/db-init-data.sql"},
    teardown={"/db-destroy.sql"},
    destroyer="destroyDs"
)
class SomeRepositoryTest {

    DataSource createDs(){
        // create your DS here...
    }

    void destroyDs(final DataSource ds){
        // destroy your ds here...
    }

    @Test @PrepareDatabase(setup="/add-more-data.sql")
    void testing(final DataSource ds){
        // do your testing
    }
}

In this example, the testing method would use the "creator", "destroyer", and "teardown" values from the annotation on the class, while the "setup" scripts would come from both the class list and the one provided in the method annotation. The DataSource argument is populated by the parameter resolver.

SharedRandomExtension

The SharedRandomExtension is used to test randomized scenarios in a way that removes the randomness for repeatable testing. It may seem odd to disable the randomness, but when you are trying to fix a failing test case, you will want to pin the "random" value so that you can fix the test and ensure that it succeeds.

This extension will only work with classes that use the SharedRandom class to provide their randomization. This includes the Randomizers defined in this library, which is actually what it was created to test.

The extension will set a known seed value on the random generator so that it is no longer random. By default, a shared known seed will be used (see DEFAULT_KNOWN_SEED); however, this may be overridden by a configured value in your test class or by the @ApplySeed annotation on your test method.

You can specify your own seed value by adding a field to your class with the signature private static final long KNOWN_SEED = <your-value> to your test class. This provided value will be used instead of the default (it does not have to be private).

Alternately, if your test method is annotated with the @ApplySeed annotation, its value will be used as the seed for that test method.

The random generator is reset after each test by setting the seed to the current nanoTime() value (i.e. making it "random" again).

A simple example of this extension in a test would be:

@ExtendWith(SharedRandomExtension.class)
class YourInterestingTest {

    @Test @ApplySeed(8675309L)
    void tester(){
        val rand = SharedRandom.current();

        assertEquals(8675309L, ((SharedRandom) rand).getSeed());
        assertEquals(-4523360879423753120L, rand.nextLong());
    }
}

The specified seed, 8675309 will be used in the SharedRandom, allowing the "random" values to be predictable.

Note
In case you are not aware, the seed-based random number generation is not really random - if you use the same seed, you get the same "random" values in the same order, which is the basis for this method of testing.

SystemPropertiesExtension

The SystemPropertiesExtension is used to update the System properties with a configured set of properties, resetting it back to the original values after each test.

In order to provide the property values to be injected, you must provide either a Properties or Map<String,String> object named "SYSTEM_PROPERTIES" on the test class as a static field.

Alternately, you may specify the field name containing your properties using the @ApplyProperties annotation on the test method.

Before each test method is executed, the configured properties will be injected into the System properties; however, the original values will be stored and replaced after the test method has finished.

@ExtendWith(SystemPropertiesExtension.class)
class SystemPropertiesExtensionPropertiesTest {

    @SuppressWarnings("unused")
    static final Properties SYSTEM_PROPERTIES = asProperties(Map.of(
        "first.name", "Bob"
    ));
    @SuppressWarnings("unused")
    static final Properties OVERLAY = asProperties(Map.of(
        "first.name", "Fred"
    ));

    @Test void checkValues() {
        assertEquals("Bob", getProperty("first.name"));
    }

    @Test @ApplyProperties("OVERLAY")
    void checkOverlayValues(){
        assertEquals("Fred", getProperty("first.name"));
    }
}
Note
Due to the global nature of the System properties, the test methods under this extension are locked so that only one should run at a time - that being said, if you run into odd issues, try executing these tests in a single-threaded manner (and/or report a bug if you feel the functionality could be improved).

ResourcesExtension

The ResourcesExtension provides for the injection of classpath resource paths or content based on object type annotated with the @Resource annotation - the supported types are as follows:

  • A Path will be populated with the path representation of the provided classpath value.

  • A File will be populated with the file representation of the provided classpath value.

  • A String will be populated with the contents of the file at the classpath location, as a String.

  • An InputStream will be populated with the content of the file at the classpath location, as an InputStream.

  • A Reader will be populated with the content of the file at the classpath location, as a Reader.

  • A byte array (byte[]) will be populated with the content of the file at the classpath location, as a array of bytes.

  • Any other object type will attempt to deserialize the contents of the file at the classpath location using the configured serdes value of the annotation (defaulting to JacksonJsonSerdes if none is specified.

The annotated types may be:

  • Static Fields. A static field annotated with the @Resource annotation will be populated during the "BeforeAll" callback.

  • Non-Static Fields. A non-static field annotated with the {@link Resource} annotation will be populated during the "BeforeEach" callback.

  • Callback or Test Method Parameters. A lifecycle callback or test method parameter annotated with the @Resource annotation will be populated when that method is called by the test framework.

A contrived example could look something like the following:

@ExtendWith(ResourcesExtension.class)
class SomeTest {
    @Resource('/resource-01.dat') static byte[] resourceData;   // injected during BeforeAll
    @Resource('/resource-02.txt') String someText;              // injected during BeforeEach

    @Test void testing(
        @Resource(value="/person.xml", serdes=JacksonXmlSerdes.class) final Person person
    ){
        // testing with the instantiated person (from xml)
    }
}

The resource loading provided by this extension delegates to the Resources utility methods, which may be used directly - this extension provides a simplification framework for common use cases.

Note
All injected fields will be cleared (set to null) during the appropriate "after" callback.

LogAppenderExtension

The LogAppenderExtension provides a test configuration framework for the InMemoryLogAppender, which allows for test collection of log messages for test result verification.

Before each test method, the extension will resolve the log appender configuration as one of the following:

  1. If the test method is annotated with the @ApplyLogging annotation, its value will be used as the name of a field of type AppenderConfig.

  2. Otherwise, a field of type AppenderConfig with the name APPENDER_CONFIG will be used.

The InMemoryLogAppender is configured with the AppenderConfig and may be accessed by the test method by adding a parameter to the method of type InMemoryLogAppender.

After each test method, the registered loggers will be detached as part of the cleanup.

An example would look something like the following:

@ExtendWith(LogAppenderExtension.class)
class SomeServiceTest {
    private static final AppenderConfig APPENDER_CONFIG = AppenderConfig.configure()
        .loggedClass(SomeService.class);

    private static final AppenderConfig OTHER_CONFIG = AppenderConfig.configure()
        .loggedClass(SomeService.class)
        .filter(evt -> evt.getLevel().isGreaterOrEqual(WARN));

    @Test void testing(final InMemoryLogAppender appender){
        // your testing with the APPENDER_CONFIG
    }

    @Test @ApplyLogging("OTHER_CONFIG")
    void otherTesting(final InMemoryLogAppender appender){
        // your testing with the OTHER_CONFIG
    }
}

Hamcrest Matchers

The test-things library provides a collection of useful Hamcrest matchers to aid in testing.

Note
Below are some of the provided matchers - see that Java Docs for a comprehensive list.

Files. The FileMatcher provides matchers for many commonly-used file-related properties.

assertThat(file, allOf(isFile(), fileExists(), isReadable()));

Date. The ChronoLocalDateMatcher supports the matching of date objects implementing the ChronoLocalDate interface, such as LocalDate instances.

assertThat(someDate, ChronoLocalDateMatcher.isAfter(otherDate));

Date Time.* The ChronoLocalDateTimeMatcher supports the matching of date-time objects implementing the ChronoLocalDateTime interface, such as the LocalDateTime instances.

assertThat(someDateTime, ChronoLocalDateTimeMatcher.isBefore(otherDateTime));

Date Operations. The TemporalMatcher supports matching of various Temporal fields.

assertThat(someDate, TemporalMatcher.isAfternoon());

Predicate Wrapper. The PredicateMatcher wraps a Predicate<T> instance to allow it to be used as a Hamcrest matcher.

assertThat(something, PredicateMatcher.matchesPredicate(v -> v > 100));

Byte Arrays. The ByteArrayMatcher provides a set of matchers for byte[] arrays, which can be tricky in testing.

assertThat(bytes, ByteArrayMatcher.arrayStartsWith(someBytes));

Atomics. The AtomicMatcher provides some matchers for matching "atomic" objects.

assertThat(counter, AtomicMatcher.atomicIntIs(equalTo(42)));

Verifiers & Resources

The library provides a Resources utility class which contains numerous methods for loading and working with classpath resources for testing. It also provides a Verifiers utility class (and other utilities with the suffix Verifier) to provide some useful common test verification methods.

Injectors

Often during testing, especially with 3rd-party libraries, you need to get some data into or out of an object that does not provide a clean means of access. In this case, reflection is your friend, but it’s an annoying friend that doesn’t always do what you want without argument.

The "injector" framework in test-things, provides a simpler means of injecting values into and extracting values out of objects with simple configuration. Also, it plays nicely with the "randomizer" framework also provided in this library.

Consider the following contrived class example, maybe from some 3rd-party library you can’t change:

public class Structure {

    private int code;

    public Structure(final int code){
        this.code = code * 42; // maybe it does some stuff with the code
    }

    public int getCode(){
        return code;
    }

    // it does other stuff with the code field
}

For a test case we are working on we want to modify the code value after construction, but it’s only provided in the constructor. Using the Injector we can update the value as needed:

var injected = Injector.inject(new Structure(1010), inj -> {
    inj.set("code", 2020);
});

// the value of "code" is now 2020

In the test, the set(String,Object) method is used to inject the desired value into the instance. When the injection is performed, it is done on the same object instance, not a clone or copy - this is not mocking, it’s just putting data into an existing field. If you inspect the object instance with the debugger you would see that the value of the code field is 2020 after the injection.

So at this point, this is nothing all that earth-shattering, it’s just reflection. The fun parts come with the flexibility of the configuration api.

In the previous example, we used the set injector to directly inject a field value. There is also a version of this method which will first try to use a setter method. For example, if the field is named foo and there is a method void setFoo(value), the injector will first try to use the setter, and if that is not present it will directly set the field value - this is an optional behavior.

Another means of injection is the update injector. Consider our example above. Maybe we only wanted to double the current value of the code field rather than setting it specifically. We could do something like the following:

var injected = Injector.inject(new Structure(1), inj -> {
    inj.update("code", c -> c * 2);
});

// the value of "code" is now: 42 x 1 x 2 = 84

The update injector applies the provided function to the current value of the specified field - in this case the value of code is 42 multiplied by 2 which ends up being 84. The value returned by the function replaces the current value.

Another useful case is when you have a mutable object in a field that you need to modify. You can use the modify injector to act on the current value without replacing it, such as in the following:

public class Something {

    private final Map<String, Integer> counts = new HashMap<>();

    // other code
}

The counts field has a map that we want to update by changing its contents, not its value. We can use the modify injector to do that:

var injected = Injector.inject(new Something(), inj -> {
    inj.modify("counts", m -> {
        m.put("alpha", 100);
        m.put("bravo", 200);
    });
});

This updates the contents of the map without replacing it.

There is no limit to the number of injections you can configure - each configuration consumer can apply multiple injectors as needed.

Lastly, if the value to be injected is an instance of the Randomizer interface, the one() method will be called to generate a random value which will be used in the injection.

Randomizers

Testing with randomized values may sound like an odd concept, but it does have its uses. Consider a case where there are too many permutations of a scenario to adequately test all of them. You could create a randomized set of test values to run against and run your test multiple times - sure, you still don’t hit them all, but you may stumble on a set that does fall into some hidden bug that your static tests would not have found.

The Test-Things library provides a Randomizer<T> interface to define a means of randomly generating objects or values. It’s primary method of interest is the T one() method, which generates one randomized object of the specific type, though you can generate multiple random instances using the List<T> many(int) method as well.

The library provides a handful of Randomizer<T> implementations, including the ObjectRandomizers which allow you to build more complex randomized objects using randomized values for the fields and properties of a given object - combining injectors with randomizers.

Given some class for which to generate random instances, such as:

public class Thing {
    ThingType type;
    int count;
    private String name;

    public void setName(final String name){
        this.name = name;
    }

    public String getName(){
        return name;
    }
}

You can generate random instances of Thing using:

val rando = ObjectRandomizers.randomized(new Thing(), inj -> {
    inj.setField("type", CoreRandomizers.oneOf(ThingType.class));
    inj.setField("count", NumberRandomizers.anIntBetween(0,1000));
    inj.setProperty("name", StringRandomizers.alphabetic(CoreRandomizers.constant(6)));
});

Which would generate a random Thing by directly injecting the type and count fields using values from the provided randomizers, while also injecting the random value for the name field by first trying to use the "setter" for the property.

With this framework, you can generate complex random instances as simply as you can generate random primitive values.

Tip
You can "pin" the randomizers so that they will produce the same values - see the SharedRandom class for details. This allows you to reproduce failing test values.

SharedRandom

All of the Randomizer<T> implementations provided in this toolkit use the SharedRandom class to provide the random values. This class is based on the standard ThreadLocalRandom class, but it provides a means of easily overriding the seed value, which is useful for "pinning" the random values for testing.

The seed value may be overridden directly in the instance, or a system property test-things.rando.seed may be set to configure the JVM-wide seed value to be used.

Capturing SLF4J Logging

Sometimes the result of an operation ends up being a log message, or the lack thereof. This can be a tricky result to unravel, but if you are using the SLF4J logging API, you can use the InMemoryLogAppender, which is based on the Logback logging implementation (the primary implementation of the SLF4J API).

To capture the log events without a lot of complex mocking, all you need to do is inject your own Appender - simply put, the Appender implementations are what collects the log events and renders them in some usable format (e.g log files).

In order to test and verify logging events, you need to create an instance of the InMemoryLogAppender and configure it with the logged classes that you want it to capture the events of.

class SomeTest {

    private final InMemoryLogAppender appender = new InMemoryLogAppender(cfg -> {
        cfg.loggedClass(Alpha.class);
        cfg.loggedClass(Bravo.class);
    });
}

In order for the appender to be registered properly with the logging system, the InMemoryLogAppender::attach() method must be called once all of the logger configuration has been performed. When the testing is done, the InMemoryLogAppender::detach() method should be called to clean up the test logger configurations. These operations are best performed in the @BeforeEach and @AfterEach methods, such as (adding to the above example):

@BeforeEach void beforeEach() {
    appender.attach();
}

@AfterEach void afterEach() {
    appender.detach();
}

Then you can run your test target and examine the generated (collected) log events in the appender with the various provided accessor methods.

Note
There is also a JUnit 5 extension that makes using this even easier - see the LogAppenderExtension for details.

Matchers

A few Hamcrest matchers have been provided specifically for use with this log testing framework:

  • The LogLevelMatcher matches criteria for the log level of the event.

  • The LogMessageMatcher matches criteria for the log message string of the event.

  • The LogNameMatcher matches criteria for the logger name for the event.

Serdes Providers

Some of the resource and verification method require serialization operations to load or verify content based on serialized or deserialized values. In order to keep things as generic as possible, Test-Things provides a SerdesProvider interface to abstract the actual "Ser"ialization and "Des"erialization operations (SerDes).

Currently, there are three provided implementations:

  • JacksonJsonSerdes - JSON-based serdes using the Jackson JSON ObjectMapper

  • JacksonXmlSerdes - XML-based serdes using the Jackson XML XmlMapper

  • JavaObjectSerdes - standard Java object binary serdes using the ObjectInputStream and ObjectOutputStream

Note
Some of the serdes-based resource and verification methods are provided without a SerdesProvider parameter, which generally means they are using the JacksonJsonSerdes provider - a sensible default.

Other Useful Testing Libraries

JUnit 5. The gold standard for Java unit testing frameworks - though if you are reading this, you probably already know that.

Ersatz Server. A useful framework for testing HTTP client code against a real server with pre-determined responses and expectations. A sister project to this library.

Hamcrest. An expressive framework for assertion matchers - the matchers in test-things are based on this framework.

Awaitility. Library providing a means of testing asynchronous code in a stable and repeatable manor.

Mockito. A powerful mocking and stubbing library.

Appendices

A. Development Philosophy

As with my other project, the Ersatz Server, I intend to keep this project as clean and useful as possible, without holding on too tightly strict rules.

Being that this is a project used in writing unit tests, I don’t generally feel the need for strict backwards compatability as long as there is a simple upgrade path. That being said, if some change causes a major problem, I am not against cutting a new release with changes that make the transition easier.

B. License

This project is licensed under the Apache 2.0 License.

Copyright (C) 2023 Christopher J. Stehno

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.