Automated Testing

From OpenSimulator

(Difference between revisions)
Jump to: navigation, search
(0.2)
 
(101 intermediate revisions by 16 users not shown)
Line 1: Line 1:
= Goal =
+
{{Quicklinks|Automated_Testing}}
To create a crossplatform, distributed continuous integration system for open simulator grids which can automate both builds and testing.
+
  
= People =  
+
== Introduction ==
The list of people who are working to develop a solution:
+
* Daedius Moskvitch (daedius @@@@@ daedius.com)
+
  
= Features =
+
OpenSimulator uses nunit to implement an automated code-level testing suite.  The suite is run by http://jenkins.opensimulator.org after every code commit on the master git branch.  It can also be run manually, as described below.
* Crossplatform
+
* Notification via email,irc,web
+
* Master-Slave architecture
+
* Nant support
+
* NUnit support
+
* Support for grid testing
+
  
= Released Versions =
+
The suite exists to reduce regression bugs, facilitate refactoring and to check functionality on different platforms, amongst other things. Patches that extend the suite or bug reports about failure are very welcome.
== 0.1 ==
+
Released: January 1st, 2008<br>
+
A simple application that can detect changes on the opensim svn, attempt compile, and return whether it was successful or not
+
* [http://buildbot.ambientunion.com/BuildBot.Net-0.1.zip Sources]
+
  
===Features===
+
== Executing Tests ==
* svn source control support
+
* svn change detection
+
* linux and windows support
+
  
===Installation===
+
=== Nant ===
Right now there is nant build files and visual studio 2005 solution.  
+
You can manually run all the tests for OpenSimulator on your system by running '''nant test''' as a nant target. This will run all the tests that are in the tree, though the database layer tests will be ignored if you haven't configured a database for them (see below).  The database layer tests only comprise a small portion of the test suite and concern only the database layer - other tests use in-memory implementations of the database layer where necessary.
  
===Special Notes For Running===
+
=== NUnit Console ===
After compiling the application, make sure you have the correct application configuration file for your operating system (the current default is Windows, it will fail on Linux if you do not change). Just rename the right App.config to "BuildBot.exe.config".
+
If you only want to run tests for one assembly you can do that using the NUnit Console. On Linux just run '''nunit-console2 OpenSim.Foo.Tests.dll''' and it will run only the tests for OpenSim.Foo.Tests.dll. If you are only making changes to 1 dll and just want a quick sanity check, this is the fastest way to do that.
  
===Other Notes===
+
=== Jenkins ===
* Deletion can be problematic on windows
+
On every commit to opensim all the tests are run on the [http://jenkins.opensimulator.org/ Jenkins build server on opensimulator.org]. The process takes about 5 minutes to build, test, and report the results back out on #opensim-dev via the osmantis bot.
* I think TortoiseSVN on win32 causes alot of trouble when deleting source control directories
+
* Need more user friendly exceptions
+
* A good way to force building, is to change the LatestTime.txt to a few days back ( to gaurantee that new changes are detected)
+
* HAPPY NEW YEARS!
+
  
===Contributors===
+
=== Database Layer Tests ===
* Daedius Moskvitch (daedius @@@@@ daedius.com)
+
The connection strings for the database dependent unit tests can be configured in the file OpenSim/Data/Tests/Resources/TestDataConnections.ini. This file is an embedded resource, so the project must be recompiled before changes to it will take effect. If the connection strings are not configured then the relevant tests will just be ignored.
  
= Planned Versions =
+
== Developing tests ==
== 0.2 ==
+
Create a more configurable build runner via xml.  This release will hopefully be a good precursor to having a real operational slave.
+
  
* Interfacing of master-slave architecture
+
As OpenSimulator matures, we are extremely interested in adding more automated verification into the OpenSimulator source tree. Testing exists not only to prevent bugs from creeping in, but also to provide fair warning that the behavior of the system has changed, and tests may need updating.
* Linux, Windows, and OSX support
+
* Initial buildbot.net slave configuration via xml
+
* Eorking slave that can multi-thread builds from masters
+
* Simple status and result chain in builds run on slaves
+
* Nant-based builds
+
* Custom nant tags for notification of status within a nant process (i.e. <status value="Running" Description="Compiling OpenSim" buildid="${build.id}"/> )
+
  
= Architecture Plans =
+
In OpenSimulator today we use NUnit tests. Our conventions are:
The Buildbot consists of a single buildmaster and one or more buildslaves, connected in a star topology. The buildmaster makes all decisions about what, when, and how to build. It sends commands to be run on the build slaves, which simply execute the commands and return the results. (certain steps involve more local decision making, where the overhead of sending a lot of commands back and forth would be inappropriate, but in general the buildmaster is responsible for everything).
+
# Tests should '''not''' exist inside runtime assemblies, as this makes nunit a production requirement
 +
# Tests should be in .Tests.dll assemblies. For instance, the tests for OpenSim.Data.SQLite.dll should be in the OpenSim.Data.SQLite.Tests.dll assembly. This allows for easy removal of test assemblies in products.
 +
# Tests should be as close to the code as possible, but not intermingled. So the tests for OpenSim/Data/SQLite should be in OpenSim/Data/SQLite/Tests/. Through the use of the '''Exclude''' keyword in prebuild.xml you can ensure that directory is part of OpenSim.Data.SQLite.Tests.dll and not OpenSim.Data.SQLite.dll. See exclude clarification in writing unit tests section.
 +
# Tests testing a class should be grouped into a test class file called xxxTest.cs, where xxx is the name of the class that is being tested.
 +
# Tests should be able to run safely in a production environment. That means that care must be taken not to damage data on the machine that it is being run.
 +
# Tests should be deterministic in other words repeatable. Avoid randomness in tests. Avoid multiple threads whenever possible - extra test modes may be created within OpenSimulator itself to facilitate single-threaded testing.  See good and bad testing practices below.
  
The buildmaster is usually fed Changes by some sort of version control system (see Change Sources), which may cause builds to be run. As the builds are performed, various status messages are produced, which are then sent to any registered Status Targets (see Status Delivery).
+
== Core Functionality Missing Unit Tests ==
  
[[Image:BuildSystemHighLevel.png]]
+
This is a list of functionality which is not covered by unit tests and is identified as highly desireable test target:
  
The buildmaster is configured and maintained by the “buildmaster admin”, who is generally the project team member responsible for build process issues. Each buildslave is maintained by a “buildslave admin”, who do not need to be quite as involved. Generally slaves are run by anyone who has an interest in seeing the project work well on their favorite platform.
+
# Database Modules (These are mysql tables)
 +
## region ban
 +
## land
 +
## landaccesslist
  
==Build Slave Connections==
+
== Good / Bad Test practices ==
 +
Creating good tests is an art, not a science. Tests are useful by how many bugs they find or how many bugs they avoid. Things you should think about in creating good tests is:
 +
* Throwing edge cases, like 0, "", or Null at parameters. This ensures that people functions are hardened against incomplete data. Many of our crashes come from the lack of this hardening showing up at just the wrong time.
 +
* Random tests are not a good idea. We need test results to be deterministic. In other words tests need to be repeatable. If you want to test for a range it is good idea to make separate tests for min and max values. Random values in fields can fail randomly. When something goes wrong for example in database schema the developer will not necessarily notice if the stored values are random. On the other hand its hard to troubleshoot randomly failing tests as you dont know which specific value caused the failure.
 +
* Tests should be independent and should not rely on another test being run, passing or failing. An excerpt from [http://xunitpatterns.com/Principles%20of%20Test%20Automation.html#Independent%20Test xUnit Patterns]:
 +
<blockquote>If tests are interdependent and (even worse) order dependent, we will be depriving ourselves of the useful feedback test failures provide. Interacting Tests [...] tend to fail in a group. The failure of a test that moved the [subject under test] into the state required by the dependent test will lead to the failure of the dependent test too. With both tests failing, how can we tell if it is because of a problem in code that both rely on in some way or is it a problem in code that only the first relies on. With both tests failing we can't tell. We are only talking about two tests here. Imagine how much worse this is with tens or hundreds of tests.</blockquote>
 +
* Only one function of the subject under test should be tested in one test. When testing a database access object, for example, write separate tests for creating DB entries, updating them and removing them.
 +
* Do not use the subject under test to set up the state for the test or to verify the result. Use a different method. When testing a database access object, for example, use raw SQL to insert the initial data into the DB, then run the method being tested. To verify if the operation was successful, use raw SQL again to verify the DB changed as expected.
 +
* Use descriptive asserts whenever you can. All you have to do is add an extra , to the Assert() method and write a string that will show when that test fails. For example:
 +
  Assert.That(i,Is.EqualTo(5),"i is not equal to 5! in Example.Test1()");
 +
* When a test fails due to a uncaught exception, such as NullReference, nUnit does not report where it happened, leaving debuggers clueless. A good practive is to write something on the start of every test in your test file. This way if an exception is raised, someone could read the last lines written and see at least in what test it failed. Luckily, this routine is already implemented in OpenSim.Tests.Common.TestHelper InMethod().
  
The buildslaves are typically run on a variety of separate machines, at least one per platform of interest. These machines connect to the buildmaster over a TCP connection to a publically-visible port. As a result, the buildslaves can live behind a NAT box or similar firewalls, as long as they can get to buildmaster. The TCP connections are initiated by the buildslave and accepted by the buildmaster, but commands and results travel both ways within this connection. The buildmaster is always in charge, so all commands travel exclusively from the buildmaster to the buildslave.
+
== Writing Tests ==
  
To perform builds, the buildslaves must typically obtain source code from a CVS/SVN/etc repository. Therefore they must also be able to reach the repository. The buildmaster provides instructions for performing builds, but does not provide the source code itself.  
+
See [http://www.nunit.org/index.php?p=quickStart&r=2.4 NUnit Quick Start] for an introduction to unit testing with NUnit.
  
[[Image:BuildSystemBuildSlaves.png]]
+
Writing a new unit test is pretty easy, and very helpful in increasing the stability of opensim by nailing down bugs. I'm going to present an example here of SQLite Asset testing to show how simple such a test case is to write. The actual in tree SQLite Asset tests are a little different because the code was factored out so that it was easily applied to any database driver, so don't be concerned with the fact that what you see here isn't in the tree.
  
==Buildmaster Architecture==
+
Exclude clarification: Make sure your master project (not the test project) has an entry for files like the following so that the test code is not included in the master project dll:
 +
     
 +
<pre>
 +
      <Files>
 +
        <Match pattern="*.cs" recurse="true">
 +
          <Exclude name="Tests" pattern="Tests" />
 +
        </Match>
 +
      </Files>
 +
</pre>
  
The Buildmaster consists of several pieces:
+
=== NUnit Conventions ===
 +
An NUnit test suite:
 +
* is a class with a default constructor (takes no arguments)
 +
* has public methods that are tests
 +
* uses annotations to determine what are tests
 +
* runs it's tests in '''alphabetical order by method name'''
  
[[Image:BuildSystemBuildMasterBuildSlaves.png]]
+
An NUnit test method:
 +
* must be public
 +
* must return void
 +
* must take no arguments
 +
* is successful if no exception or assert is thrown while running it
  
    * Change Sources, which create a Change object each time something is modified in the VC repository. Most ChangeSources listen for messages from a hook script of some sort. Some sources actively poll the repository on a regular basis. All Changes are fed to the Schedulers.
+
'''Once the tests are moved to NUnit 2.5+:'''
    * Schedulers, which decide when builds should be performed. They collect Changes into BuildRequests, which are then queued for delivery to Builders until a buildslave is available.
+
    * Builders, which control exactly how each build is performed (with a series of BuildSteps, configured in a BuildFactory). Each Build is run on a single buildslave.
+
    * Status plugins, which deliver information about the build results through protocols like HTTP, mail, and IRC.  
+
  
[[Image:BuildSystemBuildDistrobution.png]]
+
* a test class can be generic and can have one or more constructors with parameters. in this case, one or more [TestFixture(...)] attributes must be used to provide the types for the generic arguments and values for the constructors. This means it is possible to create a single test class which would, for example, provide tests for different database engines.
 +
* each test method may have parameters supplied by attributes such as [TestCase(...)] or [Values]. The test will run automatically for various combinations of these attributes.
 +
* NUnit no longer guarantees that the tests will be performed in any particular order.
  
Each Builder is configured with a list of BuildSlaves that it will use for its builds. These buildslaves are expected to behave identically: the only reason to use multiple BuildSlaves for a single Builder is to provide a measure of load-balancing.
+
The run order is important if you want to have early tests that setup some complicated state (like creating objects), and have later tests remove or update that state. For that reason I find it very helpful to name all test methods '''Txxx_somename''' where '''xxx''' is a number between 000 and 999. That guarantees no surprises in run order.
  
Within a single BuildSlave, each Builder creates its own SlaveBuilder instance. These SlaveBuilders operate independently from each other. Each gets its own base directory to work in. It is quite common to have many Builders sharing the same buildslave. For example, there might be two buildslaves: one for i386, and a second for PowerPC. There may then be a pair of Builders that do a full compile/test run, one for each architecture, and a lone Builder that creates snapshot source tarballs if the full builders complete successfully. The full builders would each run on a single buildslave, whereas the tarball creation step might run on either buildslave (since the platform doesn't matter when creating source tarballs). In this case, the mapping would look like:
+
=== Fixture Setup / Teardown ===
  
    Builder(full-i386)  ->  BuildSlaves(slave-i386)
+
See [[Example Test SQLite Assets]] for this code snipped in context.
    Builder(full-ppc)  ->  BuildSlaves(slave-ppc)
+
    Builder(source-tarball) -> BuildSlaves(slave-i386, slave-ppc)
+
  
and each BuildSlave would have two SlaveBuilders inside it, one for a full builder, and a second for the source-tarball builder.
+
<source lang="csharp">
 +
[TestFixtureSetUp]
 +
public void Init()
 +
{
 +
    uuid1 = UUID.Random();
 +
    uuid2 = UUID.Random();
 +
    uuid3 = UUID.Random();
 +
    name1 = "asset one";
 +
    name2 = "asset two";
 +
    name3 = "asset three";
  
Once a SlaveBuilder is available, the Builder pulls one or more BuildRequests off its incoming queue. (It may pull more than one if it determines that it can merge the requests together; for example, there may be multiple requests to build the current HEAD revision). These requests are merged into a single Build instance, which includes the SourceStamp that describes what exact version of the source code should be used for the build. The Build is then randomly assigned to a free SlaveBuilder and the build begins.  
+
    asset1 = new byte[100];
 +
    asset1.Initialize();
 +
    file = Path.GetTempFileName() + ".db";
 +
    connect = "URI=file:" + file + ",version=3";
 +
    db = new SQLiteAssetData();
 +
    db.Initialise(connect);
 +
}
  
==Status Delivery Architecture==
+
[TestFixtureTearDown]
 +
public void Cleanup()
 +
{
 +
    db.Dispose();
 +
    System.IO.File.Delete(file);
 +
}
 +
</source>
  
The buildmaster maintains a central Status object, to which various status plugins are connected. Through this Status object, a full hierarchy of build status objects can be obtained.
+
In the case of testing something like the database layer, we have to actually attempt to store / retrieve things from a database. Following from rule #4 of good tests, we want to make sure not to touch the production databases to run our tests, so during startup we generate a temporary file name which is guaranteed not to be an existing file on the system, and use that as our database file name. By running db.Initialize() the OpenSimulator migration code will correctly populate that database with the latest schema.
[[Image:BuildSystemStatusDelivery.png]]
+
  
The configuration file controls which status plugins are active. Each status plugin gets a reference to the top-level Status object. From there they can request information on each Builder, Build, Step, and LogFile. This query-on-demand interface is used by web plugin to create the main status page each time a web browser hits the main URL.
+
Once we are done with the tests we want to make sure we aren't leaving garbage temp files on the user's system. So we remove that file we created.
  
The status plugins can also subscribe to hear about new Builds as they occur: this is used by the MailNotifier to create new email messages for each recently-completed Build.
+
During setup we also create a set of state variables, such as 3 uuids, 3 strings, and a data block. You could have always just stuck these inline, but variables are there for a reason, so use them.
  
The Status object records the status of old builds on disk in the buildmaster's base directory. This allows it to return information about historical builds.
+
=== Test Setup / Teardown ===
  
There are also status objects that correspond to Schedulers and BuildSlaves. These allow status plugins to report information about upcoming builds, and the online/offline status of each buildslave
+
What's missing in [[Example Test SQLite Assets]] are individual test Setup and Teardown methods. These methods allow each test to be completely self sufficient without the code duplication needed to set up the test environment at the start of each test.
  
= Comparison To Other Automated Build/Testing Methods=
+
Let's assume the <code>SQLiteAssetData</code> class provided a <code>FetchAsset()</code> method and a <code>UpdateAsset()</code> method. Since every test should be independent of any other test, and <code>FetchAsset()</code> and <code>UpdateAsset()</code> should be tested in separate tests, that means each test would need to create its own entries in the asset table in order to succeed. You may have something like this (see [[Example Test SQLite Assets#This Test is Flawed|Example Test SQLite Assets]] for an explanation of <code>sqldb.executeSQL()</code>):
== CruiseControl.Net ==
+
* Supporting the master slave architecture will be vastly different from CC's centralized approach, but it will allow us more flexability and decentralization of tests on many platforms. It will also enable us to run certain tests which require multiple computers for grid testing.
+
* This project has a great system of xml specification for projects and support for Nant,Nunit,NCop, etc we could learn from thoug
+
  
== CruiseControl ==
+
<source lang="csharp">[Test]
* Once again, our project will have a focus on distributed testing and building.
+
public void TestFetchAsset()
* Our support for C# technologies will be within our control if something goes wrong. Also, knowing how mono works on all platforms is much easier than knowing how both Java and Mono work on all platforms.
+
{
 +
    AssetBase a1 = new AssetBase(...);
 +
    sqldb.executeSQL("INSERT INTO assets VALUES({0}, ...)", a1.uuid, ...);
  
== BuildBot ==
+
    AssetBase a1_actual = db.FetchAsset(a1.uuid);
* Buildbot has a great architecture for distributed builds and testing which we will be using extensively as a general direction
+
* We will not use Python as a configuration language, rather we will be using XML structures more similar in CruiseControl
+
* We will have more direct support for C# technologies
+
  
== Manual Building & Testing ==
+
    Assert.Equal(a1_actual.uuid, a1.uuid);
* More efficient
+
    Assert.Equal(a1_actual.Name, a1.Name);
* More automated
+
    // etc
* More proven
+
}
 +
 
 +
[Test]
 +
public void TestUpdateAsset()
 +
{
 +
    AssetBase a1 = new AssetBase(...);
 +
    sqldb.executeSQL("INSERT INTO assets VALUES({0}, ...)", a1.uuid, ...);
 +
 
 +
    a1.Name = "new name";
 +
 
 +
    db.UpdateAsset(a1.uuid, a1);
 +
 
 +
    AssetBase a1_actual = sqldb.executeSQL("SELECT * FROM assets WHERE uuid = {0}", a1.uuid);
 +
 
 +
    Assert.Equal(a1_actual.uuid, a1.uuid);
 +
    Assert.Equal(a1_actual.Name, a1.Name);
 +
    // etc
 +
}
 +
</source>
 +
 
 +
You will note that both tests have the same code at the top in which they create an entry in the assets table. This duplicate code can be factored out into a Setup method, which is called before every test is executed (assume <code>a1</code> is a class attribute):
 +
 
 +
<source lang="csharp">
 +
[SetUp]
 +
public void SetUp()
 +
{
 +
    a1 = new AssetBase(...);
 +
    sqldb.executeSQL("INSERT INTO assets VALUES({0}, ...)", a1.uuid, ...);
 +
}
 +
 
 +
[TearDown]
 +
public void TearDown()
 +
{
 +
    // clean up after ourselves so the next test has a clean DB to start with
 +
    sqldb.executeSQL("DELETE FROM assets");
 +
}
 +
 
 +
[Test]
 +
public void TestFetchAsset()
 +
{
 +
    AssetBase a1_actual = db.FetchAsset(a1.uuid);
 +
 
 +
    Assert.Equal(a1_actual.uuid, a1.uuid);
 +
    Assert.Equal(a1_actual.Name, a1.Name);
 +
    // etc
 +
}
 +
 
 +
[Test]
 +
public void TestUpdateAsset()
 +
{
 +
    a1.Name = "new name";
 +
 
 +
    db.UpdateAsset(a1.uuid, a1);
 +
 
 +
    AssetBase a1_actual = sqldb.executeSQL("SELECT * FROM assets WHERE uuid = {0}", a1.uuid);
 +
 
 +
    Assert.Equal(a1_actual.uuid, a1.uuid);
 +
    Assert.Equal(a1_actual.Name, a1.Name);
 +
    // etc
 +
}
 +
</source>
 +
 
 +
Also note the <code>TearDown()</code> method; it is called after each test has run, regardless whether the test passed or failed. It deletes all the entries in the <code>assets</code> table so that there is no leftover data in the database to interfere with the next test.
 +
 
 +
==== Multiple Setup Methods ====
 +
 
 +
Not all setup and teardown must happen in methods declared [SetUp] and [TearDown]. It may be useful to provide methods which perform part of the setup and to call them from whichever test may need it:
 +
 
 +
<source lang="csharp">
 +
private AssetBase InsertAssetWithRandomData(UUID assetUuid)
 +
{
 +
    AssetBase asset = new AssetBase(assetUuid);
 +
    asset.Name = somethingRandom();
 +
    asset.Data = somethingRandom();
 +
 
 +
    sqldb.executeSQL("INSERT INTO assets VALUES({0}, ...)", asset.uuid, ...);
 +
 
 +
    return asset;
 +
}
 +
 
 +
[Test]
 +
public void TestNeedsTwoAssets()
 +
{
 +
    AssetBase a1 = InsertAssetWithRandomData(uuid1);
 +
    AssetBase a2 = InsertAssetWithRandomData(uuid2);
 +
    // etc
 +
}
 +
 
 +
[Test]
 +
public void TestNeedsFiveAssets()
 +
{
 +
    AssetBase a1 = InsertAssetWithRandomData(uuid1);
 +
    AssetBase a2 = InsertAssetWithRandomData(uuid2);
 +
    AssetBase a3 = InsertAssetWithRandomData(uuid3);
 +
    AssetBase a4 = InsertAssetWithRandomData(uuid4);
 +
    AssetBase a5 = InsertAssetWithRandomData(uuid5);
 +
    // etc
 +
}
 +
</source>
 +
 
 +
Notice that <code>InsertAssetWithRandomData</code> is <code>private</code> as it's only called from within the class.
 +
 
 +
=== Asserts ===
 +
You will see scattered through the code '''Assert.That(...)'''. These will throw an exception if the condition is not valid. This format of assertions is called the [http://www.nunit.org/index.php?p=constraintModel&r=2.4 Constraint Model] in NUnit, and provides a large number of tests with the flavor of:
 +
* Assert.That(foo, Is.Null)
 +
* Assert.That(foo, Is.Not.Null)
 +
* Assert.That(foo, Is.True)
 +
* Assert.That(foo, Is.EqualTo(bar))
 +
* Assert.That(foo, Text.Matches( "*bar*" ))
 +
 
 +
Of note, Is.EqualTo uses the Equals function of foo, so this can only be used on objects that are IComparable. Most of the OpenSimulator base objects are not, so you'll have to compare fields manually in tests.
 +
 
 +
For the complete set of conditions you can use see [http://www.nunit.org/index.php?p=constraintModel&r=2.4 the Constraint Model NUnit documentation]. While there is another syntax for tests, the Constraint Model is preferred as it is far more human readable.
 +
 
 +
=== Simple Negative Tests ===
 +
 
 +
See [[Example Test SQLite Assets]] for this code snipped in context.
 +
 
 +
<source lang="csharp">[Test]
 +
public void TestLoadEmpty()
 +
{
 +
    Assert.That(db.ExistsAsset(uuid1), Is.False);
 +
    Assert.That(db.ExistsAsset(uuid2), Is.False);
 +
    Assert.That(db.ExistsAsset(uuid3), Is.False);
 +
}
 +
</source>
 +
 
 +
Test T001 is an example of a simple negative test. We assume a new database will not have any of those assets in them. While the value of this test may look low, it does provide a baseline in ensuring that the database connection is there, that these return false correctly, and that no other exception is thrown. Negative tests are a good way to force bounds conditions and ensure that not only does it ''return what you expect'' it also ''doesn't return what you don't expect''. Thought of another way, it ensures your code is somewhat defensive in nature, not coughing on bad or unexpected data.
 +
 
 +
=== Simple Positive Tests ===
 +
 
 +
See [[Example Test SQLite Assets]] for this code snipped in context.
 +
 
 +
<source lang="csharp">
 +
[Test]
 +
public void TestStoreSimpleAsset()
 +
{
 +
    AssetBase a1 = new AssetBase(uuid1, name1);
 +
    AssetBase a2 = new AssetBase(uuid2, name2);
 +
    AssetBase a3 = new AssetBase(uuid3, name3);
 +
    a1.Data = asset1;
 +
    a2.Data = asset1;
 +
    a3.Data = asset1;
 +
   
 +
    db.CreateAsset(a1);
 +
    db.CreateAsset(a2);
 +
    db.CreateAsset(a3);
 +
 
 +
    AssetBase a1a = db.FetchAsset(uuid1);
 +
    Assert.That(a1a.ID, Is.EqualTo(uuid1));
 +
    Assert.That(a1a.Name, Is.EqualTo(name1));
 +
 
 +
    AssetBase a2a = db.FetchAsset(uuid2);
 +
    Assert.That(a2a.ID, Is.EqualTo(uuid2));
 +
    Assert.That(a2a.Name, Is.EqualTo(name2));
 +
 
 +
    AssetBase a3a = db.FetchAsset(uuid3);
 +
    Assert.That(a3a.ID, Is.EqualTo(uuid3));
 +
    Assert.That(a3a.Name, Is.EqualTo(name3));
 +
}
 +
</source>
 +
T010 is an example of a simple positive test. In it we create and store 3 assets (ensuring no exceptions), then load those 3 assets back from the database and ensure the fields are correct. Because AssetBase is not IComparible we just check the ID and Name fields with equals tests. If any of the Asserts fail, the whole test fails.
 +
 
 +
=== Speculative Tests ===
 +
Speculative tests are tests that might or might not apply in a given situation. MySQL testing in the OpenSimulator tree is done by speculative testing, the tests will only run if there is a properly configured database, otherwise they will not be run. If you execute '''Assert.Ignore()''' in a '''Test''' the test will end and be ignored. If you run '''Assert.Ignore()''' in the '''TestFixtureSetup''' all tests in the test fixture will be skipped and ignored.
 +
 
 +
Speculative testing lets you create tests that require certain preconditions to be met which you can't guarantee on all platforms/configuration, and are an important part of deep testing.
 +
 
 +
== Adding Tests to the Tree ==
 +
As we said previously all tests for assembly OpenSim.Foo (in directory OpenSim/Foo) should:
 +
* be in assembly OpenSim.Foo.Tests.dll
 +
* not be in the OpenSim.Foo.dll assembly
 +
* be in OpenSim/Foo/Tests directory
 +
 
 +
Also, if you have created a new test assembly you must add references
 +
to it in ''.nant/local.include'
 +
to ensure that the assembly is added to the automated continuous integration test server as
 +
well as the nant ''test'' target.
 +
 
 +
For example
 +
 
 +
<exec program="${nunitcmd}" failonerror="true" resultproperty="testresult.opensim.my.new.tests">
 +
  <arg value="./bin/OpenSim.Framework.My.New.Tests.dll" />
 +
</exec>
 +
<fail message="Failures reported in unit tests." unless="${int::parse(testresult.opensim.my.new.tests)==0}" />
 +
 
 +
=== Debugging Tests ===
 +
There is a special page dedicated to this. See [[Debugging Unit Tests]].
 +
 
 +
== Learning More ==
 +
You should definitely read the documentation at the [http://www.nunit.org/index.php?p=documentation NUnit homepage] if you want to know more about testing. It's a very good reference for all the APIs in NUnit that you can use for creating tests.
 +
 
 +
== Links to More Information on Unit Testing ==
 +
 
 +
* [http://www.nunit.org/index.php?p=quickStart&r=2.4 NUnit Quick Start]
 +
* [http://xunitpatterns.com/ xUnit Patterns book homepage], with lots of information on good practices, patterns, code smells, etc.
 +
* There is a lot of information on unit testing at the [http://c2.com/cgi/wiki?search=unittest Cunningham & Cunningham, Inc wiki at c2.com]
 +
 
 +
[[Category:Development]]
 +
[[Category:Testing]]

Latest revision as of 04:28, 4 December 2023

Contents

[edit] Introduction

OpenSimulator uses nunit to implement an automated code-level testing suite. The suite is run by http://jenkins.opensimulator.org after every code commit on the master git branch. It can also be run manually, as described below.

The suite exists to reduce regression bugs, facilitate refactoring and to check functionality on different platforms, amongst other things. Patches that extend the suite or bug reports about failure are very welcome.

[edit] Executing Tests

[edit] Nant

You can manually run all the tests for OpenSimulator on your system by running nant test as a nant target. This will run all the tests that are in the tree, though the database layer tests will be ignored if you haven't configured a database for them (see below). The database layer tests only comprise a small portion of the test suite and concern only the database layer - other tests use in-memory implementations of the database layer where necessary.

[edit] NUnit Console

If you only want to run tests for one assembly you can do that using the NUnit Console. On Linux just run nunit-console2 OpenSim.Foo.Tests.dll and it will run only the tests for OpenSim.Foo.Tests.dll. If you are only making changes to 1 dll and just want a quick sanity check, this is the fastest way to do that.

[edit] Jenkins

On every commit to opensim all the tests are run on the Jenkins build server on opensimulator.org. The process takes about 5 minutes to build, test, and report the results back out on #opensim-dev via the osmantis bot.

[edit] Database Layer Tests

The connection strings for the database dependent unit tests can be configured in the file OpenSim/Data/Tests/Resources/TestDataConnections.ini. This file is an embedded resource, so the project must be recompiled before changes to it will take effect. If the connection strings are not configured then the relevant tests will just be ignored.

[edit] Developing tests

As OpenSimulator matures, we are extremely interested in adding more automated verification into the OpenSimulator source tree. Testing exists not only to prevent bugs from creeping in, but also to provide fair warning that the behavior of the system has changed, and tests may need updating.

In OpenSimulator today we use NUnit tests. Our conventions are:

  1. Tests should not exist inside runtime assemblies, as this makes nunit a production requirement
  2. Tests should be in .Tests.dll assemblies. For instance, the tests for OpenSim.Data.SQLite.dll should be in the OpenSim.Data.SQLite.Tests.dll assembly. This allows for easy removal of test assemblies in products.
  3. Tests should be as close to the code as possible, but not intermingled. So the tests for OpenSim/Data/SQLite should be in OpenSim/Data/SQLite/Tests/. Through the use of the Exclude keyword in prebuild.xml you can ensure that directory is part of OpenSim.Data.SQLite.Tests.dll and not OpenSim.Data.SQLite.dll. See exclude clarification in writing unit tests section.
  4. Tests testing a class should be grouped into a test class file called xxxTest.cs, where xxx is the name of the class that is being tested.
  5. Tests should be able to run safely in a production environment. That means that care must be taken not to damage data on the machine that it is being run.
  6. Tests should be deterministic in other words repeatable. Avoid randomness in tests. Avoid multiple threads whenever possible - extra test modes may be created within OpenSimulator itself to facilitate single-threaded testing. See good and bad testing practices below.

[edit] Core Functionality Missing Unit Tests

This is a list of functionality which is not covered by unit tests and is identified as highly desireable test target:

  1. Database Modules (These are mysql tables)
    1. region ban
    2. land
    3. landaccesslist

[edit] Good / Bad Test practices

Creating good tests is an art, not a science. Tests are useful by how many bugs they find or how many bugs they avoid. Things you should think about in creating good tests is:

  • Throwing edge cases, like 0, "", or Null at parameters. This ensures that people functions are hardened against incomplete data. Many of our crashes come from the lack of this hardening showing up at just the wrong time.
  • Random tests are not a good idea. We need test results to be deterministic. In other words tests need to be repeatable. If you want to test for a range it is good idea to make separate tests for min and max values. Random values in fields can fail randomly. When something goes wrong for example in database schema the developer will not necessarily notice if the stored values are random. On the other hand its hard to troubleshoot randomly failing tests as you dont know which specific value caused the failure.
  • Tests should be independent and should not rely on another test being run, passing or failing. An excerpt from xUnit Patterns:
If tests are interdependent and (even worse) order dependent, we will be depriving ourselves of the useful feedback test failures provide. Interacting Tests [...] tend to fail in a group. The failure of a test that moved the [subject under test] into the state required by the dependent test will lead to the failure of the dependent test too. With both tests failing, how can we tell if it is because of a problem in code that both rely on in some way or is it a problem in code that only the first relies on. With both tests failing we can't tell. We are only talking about two tests here. Imagine how much worse this is with tens or hundreds of tests.
  • Only one function of the subject under test should be tested in one test. When testing a database access object, for example, write separate tests for creating DB entries, updating them and removing them.
  • Do not use the subject under test to set up the state for the test or to verify the result. Use a different method. When testing a database access object, for example, use raw SQL to insert the initial data into the DB, then run the method being tested. To verify if the operation was successful, use raw SQL again to verify the DB changed as expected.
  • Use descriptive asserts whenever you can. All you have to do is add an extra , to the Assert() method and write a string that will show when that test fails. For example:
 Assert.That(i,Is.EqualTo(5),"i is not equal to 5! in Example.Test1()");
  • When a test fails due to a uncaught exception, such as NullReference, nUnit does not report where it happened, leaving debuggers clueless. A good practive is to write something on the start of every test in your test file. This way if an exception is raised, someone could read the last lines written and see at least in what test it failed. Luckily, this routine is already implemented in OpenSim.Tests.Common.TestHelper InMethod().

[edit] Writing Tests

See NUnit Quick Start for an introduction to unit testing with NUnit.

Writing a new unit test is pretty easy, and very helpful in increasing the stability of opensim by nailing down bugs. I'm going to present an example here of SQLite Asset testing to show how simple such a test case is to write. The actual in tree SQLite Asset tests are a little different because the code was factored out so that it was easily applied to any database driver, so don't be concerned with the fact that what you see here isn't in the tree.

Exclude clarification: Make sure your master project (not the test project) has an entry for files like the following so that the test code is not included in the master project dll:

      <Files>
        <Match pattern="*.cs" recurse="true">
          <Exclude name="Tests" pattern="Tests" />
        </Match>
      </Files>

[edit] NUnit Conventions

An NUnit test suite:

  • is a class with a default constructor (takes no arguments)
  • has public methods that are tests
  • uses annotations to determine what are tests
  • runs it's tests in alphabetical order by method name

An NUnit test method:

  • must be public
  • must return void
  • must take no arguments
  • is successful if no exception or assert is thrown while running it

Once the tests are moved to NUnit 2.5+:

  • a test class can be generic and can have one or more constructors with parameters. in this case, one or more [TestFixture(...)] attributes must be used to provide the types for the generic arguments and values for the constructors. This means it is possible to create a single test class which would, for example, provide tests for different database engines.
  • each test method may have parameters supplied by attributes such as [TestCase(...)] or [Values]. The test will run automatically for various combinations of these attributes.
  • NUnit no longer guarantees that the tests will be performed in any particular order.

The run order is important if you want to have early tests that setup some complicated state (like creating objects), and have later tests remove or update that state. For that reason I find it very helpful to name all test methods Txxx_somename where xxx is a number between 000 and 999. That guarantees no surprises in run order.

[edit] Fixture Setup / Teardown

See Example Test SQLite Assets for this code snipped in context.

[TestFixtureSetUp]
public void Init()
{
    uuid1 = UUID.Random();
    uuid2 = UUID.Random();
    uuid3 = UUID.Random();
    name1 = "asset one";
    name2 = "asset two";
    name3 = "asset three";
 
    asset1 = new byte[100];
    asset1.Initialize();
    file = Path.GetTempFileName() + ".db";
    connect = "URI=file:" + file + ",version=3";
    db = new SQLiteAssetData();
    db.Initialise(connect);
}
 
[TestFixtureTearDown]
public void Cleanup()
{
    db.Dispose();
    System.IO.File.Delete(file);
}

In the case of testing something like the database layer, we have to actually attempt to store / retrieve things from a database. Following from rule #4 of good tests, we want to make sure not to touch the production databases to run our tests, so during startup we generate a temporary file name which is guaranteed not to be an existing file on the system, and use that as our database file name. By running db.Initialize() the OpenSimulator migration code will correctly populate that database with the latest schema.

Once we are done with the tests we want to make sure we aren't leaving garbage temp files on the user's system. So we remove that file we created.

During setup we also create a set of state variables, such as 3 uuids, 3 strings, and a data block. You could have always just stuck these inline, but variables are there for a reason, so use them.

[edit] Test Setup / Teardown

What's missing in Example Test SQLite Assets are individual test Setup and Teardown methods. These methods allow each test to be completely self sufficient without the code duplication needed to set up the test environment at the start of each test.

Let's assume the SQLiteAssetData class provided a FetchAsset() method and a UpdateAsset() method. Since every test should be independent of any other test, and FetchAsset() and UpdateAsset() should be tested in separate tests, that means each test would need to create its own entries in the asset table in order to succeed. You may have something like this (see Example Test SQLite Assets for an explanation of sqldb.executeSQL()):

[Test]
public void TestFetchAsset()
{
    AssetBase a1 = new AssetBase(...);
    sqldb.executeSQL("INSERT INTO assets VALUES({0}, ...)", a1.uuid, ...);
 
    AssetBase a1_actual = db.FetchAsset(a1.uuid);
 
    Assert.Equal(a1_actual.uuid, a1.uuid);
    Assert.Equal(a1_actual.Name, a1.Name);
    // etc
}
 
[Test]
public void TestUpdateAsset()
{
    AssetBase a1 = new AssetBase(...);
    sqldb.executeSQL("INSERT INTO assets VALUES({0}, ...)", a1.uuid, ...);
 
    a1.Name = "new name";
 
    db.UpdateAsset(a1.uuid, a1);
 
    AssetBase a1_actual = sqldb.executeSQL("SELECT * FROM assets WHERE uuid = {0}", a1.uuid);
 
    Assert.Equal(a1_actual.uuid, a1.uuid);
    Assert.Equal(a1_actual.Name, a1.Name);
    // etc
}

You will note that both tests have the same code at the top in which they create an entry in the assets table. This duplicate code can be factored out into a Setup method, which is called before every test is executed (assume a1 is a class attribute):

[SetUp]
public void SetUp()
{
    a1 = new AssetBase(...);
    sqldb.executeSQL("INSERT INTO assets VALUES({0}, ...)", a1.uuid, ...);
}
 
[TearDown]
public void TearDown()
{
    // clean up after ourselves so the next test has a clean DB to start with
    sqldb.executeSQL("DELETE FROM assets");
}
 
[Test]
public void TestFetchAsset()
{
    AssetBase a1_actual = db.FetchAsset(a1.uuid);
 
    Assert.Equal(a1_actual.uuid, a1.uuid);
    Assert.Equal(a1_actual.Name, a1.Name);
    // etc
}
 
[Test]
public void TestUpdateAsset()
{
    a1.Name = "new name";
 
    db.UpdateAsset(a1.uuid, a1);
 
    AssetBase a1_actual = sqldb.executeSQL("SELECT * FROM assets WHERE uuid = {0}", a1.uuid);
 
    Assert.Equal(a1_actual.uuid, a1.uuid);
    Assert.Equal(a1_actual.Name, a1.Name);
    // etc
}

Also note the TearDown() method; it is called after each test has run, regardless whether the test passed or failed. It deletes all the entries in the assets table so that there is no leftover data in the database to interfere with the next test.

[edit] Multiple Setup Methods

Not all setup and teardown must happen in methods declared [SetUp] and [TearDown]. It may be useful to provide methods which perform part of the setup and to call them from whichever test may need it:

private AssetBase InsertAssetWithRandomData(UUID assetUuid)
{
    AssetBase asset = new AssetBase(assetUuid);
    asset.Name = somethingRandom();
    asset.Data = somethingRandom();
 
    sqldb.executeSQL("INSERT INTO assets VALUES({0}, ...)", asset.uuid, ...);
 
    return asset;
}
 
[Test]
public void TestNeedsTwoAssets()
{
    AssetBase a1 = InsertAssetWithRandomData(uuid1);
    AssetBase a2 = InsertAssetWithRandomData(uuid2);
    // etc
}
 
[Test]
public void TestNeedsFiveAssets()
{
    AssetBase a1 = InsertAssetWithRandomData(uuid1);
    AssetBase a2 = InsertAssetWithRandomData(uuid2);
    AssetBase a3 = InsertAssetWithRandomData(uuid3);
    AssetBase a4 = InsertAssetWithRandomData(uuid4);
    AssetBase a5 = InsertAssetWithRandomData(uuid5);
    // etc
}

Notice that InsertAssetWithRandomData is private as it's only called from within the class.

[edit] Asserts

You will see scattered through the code Assert.That(...). These will throw an exception if the condition is not valid. This format of assertions is called the Constraint Model in NUnit, and provides a large number of tests with the flavor of:

  • Assert.That(foo, Is.Null)
  • Assert.That(foo, Is.Not.Null)
  • Assert.That(foo, Is.True)
  • Assert.That(foo, Is.EqualTo(bar))
  • Assert.That(foo, Text.Matches( "*bar*" ))

Of note, Is.EqualTo uses the Equals function of foo, so this can only be used on objects that are IComparable. Most of the OpenSimulator base objects are not, so you'll have to compare fields manually in tests.

For the complete set of conditions you can use see the Constraint Model NUnit documentation. While there is another syntax for tests, the Constraint Model is preferred as it is far more human readable.

[edit] Simple Negative Tests

See Example Test SQLite Assets for this code snipped in context.

[Test]
public void TestLoadEmpty()
{
    Assert.That(db.ExistsAsset(uuid1), Is.False);
    Assert.That(db.ExistsAsset(uuid2), Is.False);
    Assert.That(db.ExistsAsset(uuid3), Is.False);
}

Test T001 is an example of a simple negative test. We assume a new database will not have any of those assets in them. While the value of this test may look low, it does provide a baseline in ensuring that the database connection is there, that these return false correctly, and that no other exception is thrown. Negative tests are a good way to force bounds conditions and ensure that not only does it return what you expect it also doesn't return what you don't expect. Thought of another way, it ensures your code is somewhat defensive in nature, not coughing on bad or unexpected data.

[edit] Simple Positive Tests

See Example Test SQLite Assets for this code snipped in context.

[Test]
public void TestStoreSimpleAsset()
{
    AssetBase a1 = new AssetBase(uuid1, name1);
    AssetBase a2 = new AssetBase(uuid2, name2);
    AssetBase a3 = new AssetBase(uuid3, name3);
    a1.Data = asset1;
    a2.Data = asset1;
    a3.Data = asset1;
 
    db.CreateAsset(a1);
    db.CreateAsset(a2);
    db.CreateAsset(a3);
 
    AssetBase a1a = db.FetchAsset(uuid1);
    Assert.That(a1a.ID, Is.EqualTo(uuid1));
    Assert.That(a1a.Name, Is.EqualTo(name1));
 
    AssetBase a2a = db.FetchAsset(uuid2);
    Assert.That(a2a.ID, Is.EqualTo(uuid2));
    Assert.That(a2a.Name, Is.EqualTo(name2));
 
    AssetBase a3a = db.FetchAsset(uuid3);
    Assert.That(a3a.ID, Is.EqualTo(uuid3));
    Assert.That(a3a.Name, Is.EqualTo(name3));
}

T010 is an example of a simple positive test. In it we create and store 3 assets (ensuring no exceptions), then load those 3 assets back from the database and ensure the fields are correct. Because AssetBase is not IComparible we just check the ID and Name fields with equals tests. If any of the Asserts fail, the whole test fails.

[edit] Speculative Tests

Speculative tests are tests that might or might not apply in a given situation. MySQL testing in the OpenSimulator tree is done by speculative testing, the tests will only run if there is a properly configured database, otherwise they will not be run. If you execute Assert.Ignore() in a Test the test will end and be ignored. If you run Assert.Ignore() in the TestFixtureSetup all tests in the test fixture will be skipped and ignored.

Speculative testing lets you create tests that require certain preconditions to be met which you can't guarantee on all platforms/configuration, and are an important part of deep testing.

[edit] Adding Tests to the Tree

As we said previously all tests for assembly OpenSim.Foo (in directory OpenSim/Foo) should:

  • be in assembly OpenSim.Foo.Tests.dll
  • not be in the OpenSim.Foo.dll assembly
  • be in OpenSim/Foo/Tests directory

Also, if you have created a new test assembly you must add references to it in .nant/local.include' to ensure that the assembly is added to the automated continuous integration test server as well as the nant test target.

For example

<exec program="${nunitcmd}" failonerror="true" resultproperty="testresult.opensim.my.new.tests">
  <arg value="./bin/OpenSim.Framework.My.New.Tests.dll" />
</exec>
<fail message="Failures reported in unit tests." unless="${int::parse(testresult.opensim.my.new.tests)==0}" />

[edit] Debugging Tests

There is a special page dedicated to this. See Debugging Unit Tests.

[edit] Learning More

You should definitely read the documentation at the NUnit homepage if you want to know more about testing. It's a very good reference for all the APIs in NUnit that you can use for creating tests.

[edit] Links to More Information on Unit Testing

Personal tools
General
About This Wiki