Browse Source

Add basic unit test support

Add class that can automatically perform defined tests on user request.

Developers that wish to implemet unit tests for some functionality
must extend that class (`TestCase`) and add it to the manifest,
so that Acedia can read, register and later use it to perform tests.
new
Anton Tarasenko 5 years ago
parent
commit
a4a1c21cd7
  1. 9
      sources/Acedia.uc
  2. 3
      sources/Manifest.uc
  3. 252
      sources/TestCase.uc

9
sources/Acedia.uc

@ -31,6 +31,9 @@ var private Acedia selfReference;
// Array of predefined services that must be started along with Acedia mutator. // Array of predefined services that must be started along with Acedia mutator.
var private array< class<Service> > systemServices; var private array< class<Service> > systemServices;
// All unit tests loaded from all packages.
var private array< class<TestCase> > testCases;
static public final function Acedia GetInstance() static public final function Acedia GetInstance()
{ {
return default.selfReference; return default.selfReference;
@ -69,6 +72,12 @@ private final function LoadManifest(class<Manifest> manifestClass)
manifestClass.default.features[i].static.EnableMe(); manifestClass.default.features[i].static.EnableMe();
} }
} }
// Load unit tests
for (i = 0; i < manifestClass.default.testCases.length; i += 1)
{
if (manifestClass.default.testCases[i] == none) continue;
testCases[testCases.length] = manifestClass.default.testCases[i];
}
} }
private final function LaunchServices() private final function LaunchServices()

3
sources/Manifest.uc

@ -26,6 +26,9 @@
// List of features in this manifest's package. // List of features in this manifest's package.
var public const array< class<Feature> > features; var public const array< class<Feature> > features;
// List of features in this manifest's package.
var public const array< class<TestCase> > testCases;
// Listeners listed here will be automatically activated. // Listeners listed here will be automatically activated.
var public const array< class<Listener> > requiredListeners; var public const array< class<Listener> > requiredListeners;

252
sources/TestCase.uc

@ -0,0 +1,252 @@
/**
* Base class aimed to contain sets of unit tests for various components of
* Acedia and it's features.
* Currently provides bare-bones testing functions that check boolean
* variables for true/false and objects for whether they're `none` or not.
* Tests:
* ~ can be grouped by their "context",
* describing what they are testing;
* ~ test (or several tests) can be assigned an error message,
* describing what exactly went wrong.
* Copyright 2020 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class TestCase extends Actor
abstract;
// Name by which this set of unit tests can be referred to.
var protected const string caseName;
// Information about how well testing went for a particular context,
// i.e. subsets of tests for a particular functionality.
struct ContextSummary
{
// Text, human-readable description of the purpose of
// tests in this context.
var string description;
// `false` if at least one test failed, `true` otherwise.
var bool passed;
// How many test were performed.
var int testsPerformed;
// How many tests failed.
var int testsFailed;
// Error messages generated by failed tests.
var array<string> errors;
};
// Collection of summaries for all contexts defined by the user so far.
struct Summary
{
var bool passed;
var array<ContextSummary> contextSummaries;
};
// Has function for defining context (`Context()`) been called.
var private bool userDefinedContext;
// Were all tests performed?
var private bool finishedTests;
// Error message that will be generated if some test will fail now.
var private string currentErrorMessage;
// Store complete summary here.
var private Summary currentSummary;
// For quick access store current context's summary here and update it in
// `currentSummary` once done.
var private ContextSummary activeContextSummary;
// Call this function to define a context for subsequent test
// (until another call).
public final static function Context(string description)
{
if ( default.userDefinedContext
|| default.activeContextSummary.testsPerformed > 0)
{
UpdateContextSummary(default.activeContextSummary);
}
default.userDefinedContext = true;
default.activeContextSummary = GetContextSummary(description);
default.currentErrorMessage = "";
}
// Call this function to define an error message for tests that
// would fail after it.
// Message is reset by another call of `Issue()` or
// by changing the context via `Context()`.
public final static function Issue(string errorMessage)
{
default.currentErrorMessage = errorMessage;
}
// All tests to be performed can be placed in this function,
// along with appropriate calls to `Context()` and `Issue()`.
// For an example see class `TEST_JSON`.
protected static function TESTS(){}
// Following functions provide simple test primitives,
public final static function TEST_ExpectTrue(bool result)
{
RecordTestResult(result, default.currentErrorMessage);
}
public final static function TEST_ExpectFalse(bool result)
{
RecordTestResult(!result, default.currentErrorMessage);
}
public final static function TEST_ExpectNone(Object object)
{
RecordTestResult(object == none, default.currentErrorMessage);
}
public final static function TEST_ExpectNotNone(Object object)
{
RecordTestResult(object != none, default.currentErrorMessage);
}
// Returns the summary of how testing went.
public final static function Summary GetSummary()
{
return default.currentSummary;
}
// Name by which this set of unit tests can be referred to.
public final static function string GetName()
{
return default.caseName;
}
// Creates brand new summary for context with a given description,
// marked as "passed" and zero tests done.
private final static function ContextSummary NewContextSummary
(
string description
)
{
local ContextSummary newSummary;
newSummary.passed = true;
newSummary.description = description;
newSummary.testsPerformed = 0;
newSummary.testsFailed = 0;
newSummary.errors.length = 0;
return newSummary;
}
// Returns index of summary with given description
// in our records (`currentSummary`).
// Return `-1` if there is no context with such description.
private final static function int GetContextSummaryIndex(string description)
{
local int i;
for (i = 0; i < default.currentSummary.contextSummaries.length; i += 1)
{
if ( default.currentSummary.contextSummaries[i].description
~= description)
{
return i;
}
}
return -1;
}
// Returns index summary with given description
// in our records.
// Return new context summary if there is no context with such description.
private final static function ContextSummary GetContextSummary
(
string description
)
{
local int index;
if (default.activeContextSummary.description ~= description)
{
return default.activeContextSummary;
}
index = GetContextSummaryIndex(description);
if (index < 0)
{
return NewContextSummary(description);
}
return default.currentSummary.contextSummaries[index];
}
// Rewrites summary (with the same name as a given summary)
// in `currentSummary` records.
// If there's no such record - adds a new one.
private final static function UpdateContextSummary
(
ContextSummary relevantSummary
)
{
local int index;
index = GetContextSummaryIndex(relevantSummary.description);
if (index < 0)
{
index = default.currentSummary.contextSummaries.length;
}
default.currentSummary.contextSummaries[index] = relevantSummary;
}
// Records (in current context summary) that another test was performed and
// succeeded/failed, along with given error message.
private final static function RecordTestResult
(
bool isSuccessful,
string errorMessage
)
{
local int i;
local int errorsAmount;
if (default.finishedTests) return;
default.activeContextSummary.testsPerformed += 1;
if (isSuccessful) return;
default.currentSummary.passed = false;
default.activeContextSummary.passed = false;
default.activeContextSummary.testsFailed += 1;
errorsAmount = default.activeContextSummary.errors.length;
for (i = 0; i < errorsAmount; i += 1)
{
if (default.activeContextSummary.errors[i] ~= errorMessage)
{
return;
}
}
default.activeContextSummary.errors[errorsAmount] = errorMessage;
}
// Calling this function will perform unit tests defined in `TESTS()`
// function of this test case and will prepare the summary,
// obtainable through `GetSummary()` function.
// Returns `true` if all tests have successfully passed
// and `false` otherwise.
public final static function bool PerformTests()
{
default.finishedTests = false;
default.userDefinedContext = false;
default.currentSummary.passed = true;
default.currentSummary.contextSummaries.length = 0;
default.activeContextSummary = NewContextSummary("");
TESTS();
UpdateContextSummary(default.activeContextSummary);
default.finishedTests = true;
return default.currentSummary.passed;
}
defaultproperties
{
caseName = ""
}