Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow definition of parameterized tests by subclassing TestSuite #13

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/+xunit/+utils/isTestSuiteSubclass.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function tf = isTestSuiteSubclass(name)

class_meta = meta.class.fromName(name);
if isempty(class_meta)
% Not the name of a class
tf = false;
else
tf = class_meta < ?TestSuite;
end
51 changes: 40 additions & 11 deletions src/TestSuite.m
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,11 @@ function keepMatchingTestCase(self, name)
if xunit.utils.isTestCaseSubclass(name)
suite = TestSuite.fromTestCaseClassName(name);

elseif xunit.utils.isTestSuiteSubclass(name)
suite = feval(name);

elseif ~isempty(meta.class.fromName(name))
% Input is the name of a class that is not a TestCase subclass.
% Input is the name of a class that is not a TestCase or TestSuite subclass.
% Return an empty test suite.
suite = TestSuite();
suite.Name = name;
Expand Down Expand Up @@ -302,15 +305,39 @@ function keepMatchingTestCase(self, name)
test_suite.Name = pwd;
test_suite.Location = pwd;

mfiles = dir(fullfile('.', '*.m'));
for k = 1:numel(mfiles)
[path, name] = fileparts(mfiles(k).name);
if xunit.utils.isTestCaseSubclass(name)
test_suite.add(TestSuite.fromTestCaseClassName(name));
elseif xunit.utils.isTestString(name)
suite_k = TestSuite.fromName(name);
if ~isempty(suite_k.TestComponents)
test_suite.add(suite_k);
list = dir();
for k = 1:numel(list)
if list(k).isdir
% Directories
if strncmpi(list(k).name, '@', 1)
% Class directory
name = list(k).name(2:end);
if xunit.utils.isTestCaseSubclass(name)
test_suite.add(TestSuite.fromTestCaseClassName(name));
elseif xunit.utils.isTestSuiteSubclass(name)
test_suite.add(feval(name));
end
elseif strncmpi(list(k).name, '+', 1)
% Package directory
name = list(k).name(2:end);
test_suite.add(TestSuite.fromPackageName(name));
end
else
% Files
[~,~,e] = fileparts(list(k).name);
if strcmpi(e, '.m')
% M-files
[~, name] = fileparts(list(k).name);
if xunit.utils.isTestCaseSubclass(name)
test_suite.add(TestSuite.fromTestCaseClassName(name));
elseif xunit.utils.isTestSuiteSubclass(name)
test_suite.add(feval(name));
elseif xunit.utils.isTestString(name)
suite_k = TestSuite.fromName(name);
if ~isempty(suite_k.TestComponents)
test_suite.add(suite_k);
end
end
end
end
end
Expand All @@ -321,7 +348,7 @@ function keepMatchingTestCase(self, name)
% test_suite = TestSuite.fromPackageName(name) constructs a
% TestSuite object from all the test components found in the
% specified package.

package_info = meta.package.fromName(name);
if isempty(package_info)
error('xunit:fromPackageName:invalidName', ...
Expand All @@ -346,6 +373,8 @@ function keepMatchingTestCase(self, name)
class_names{k} = class_name;
if xunit.utils.isTestCaseSubclass(class_name)
test_suite.add(TestSuite.fromTestCaseClassName(class_name));
elseif xunit.utils.isTestSuiteSubclass(class_name)
test_suite.add(feval(class_name));
end
end

Expand Down
37 changes: 37 additions & 0 deletions tests/+xunit/+mocktests/C.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
% Class C is a TestSuite subclass containing two test cases (test_a and test_b).
classdef C < TestSuite
methods
function self = C()
self = self@TestSuite();
self.Name = class(self);
self.Location = fileparts(which(class(self)));

names = {'test_a'; 'test_b'};

testCases = self.createTestCases(names);
self.add(testCases);
end

function testCases = createTestCases(self, names)
testCases = cell(1, size(names, 1));
for idx = 1:size(names, 1)
testCases{idx} = self.createTestCase(names{idx, 1});
end
end

function testCase = createTestCase(self, name)
%CREATETESTCASE Creates a function handle test case for a given test case

% Create TestCase object
testFcn = @() [];
setupFcn = [];
teardownFcn = [];

testCase = FunctionHandleTestCase(testFcn, setupFcn, teardownFcn);

% Set name and location of test from name of file
testCase.Name = name;
testCase.Location = which(class(self));
end
end
end
46 changes: 46 additions & 0 deletions tests/TestParameterizedTests.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
%TestParameterizedTests Unit tests for sub-classing the TestSuite class.

classdef TestParameterizedTests < TestCaseInDir
methods
function self = TestParameterizedTests(name)
self = self@TestCaseInDir(name, ...
fullfile(fileparts(which(mfilename)), 'helper_classes'));
end

function testConstructor(~)
% Exercise the constructor. Verify that the Name and Location
% properties are set correctly.
ts = TestParameterizedSuite();
assertEqual(ts.Name, 'TestParameterizedSuite');
assertEqual(ts.Location, which('TestParameterizedSuite'));
assertEqual(numel(ts.TestComponents), 3);
end

function testPassingTest(~)
logger = TestRunLogger();
suite = TestParameterizedSuite();
tc = findTestComponent(suite, 'test_sin_0deg');
tc.run(logger);
assertEqual(logger.NumFailures, 0)
end

function testFailingTest(~)
logger = TestRunLogger();
suite = TestParameterizedSuite();
tc = findTestComponent(suite, 'test_sin_90deg');
tc.run(logger);
assertEqual(logger.NumFailures, 1)
end
end
end

function theTestComponent = findTestComponent(suite, name)
% Find the TestComponent given a name of a function under test.

componentNames = cellfun(@(x) x.Name, suite.TestComponents, 'UniformOutput', false);
index = find(strcmp(name, componentNames), 1);
assertTrue(~ isempty(index), ['Could not find test component for ' name]);

theTestComponent = suite.TestComponents{index};

end
6 changes: 2 additions & 4 deletions tests/TestSuiteTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,7 @@ function test_fromPwd(self)
% test components.
suite = TestSuite.fromPwd();
assertTrue(isa(suite, 'TestSuite'));
assertTrue(numel(suite.TestComponents) == 16);
assertEqual(numel(suite.TestComponents), 17);
end

end

end
end
46 changes: 46 additions & 0 deletions tests/helper_classes/TestParameterizedSuite.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
classdef TestParameterizedSuite < TestSuite
methods
function this = TestParameterizedSuite()
this = this@TestSuite();
this.Name = class(this);
this.Location = which(class(this));

values = {0, 0; 30 0.5; 90 0.8};

testCases = this.createTestCases(values);
this.add(testCases);
end

function testCases = createTestCases(this, values)
testCases = cell(1, size(values, 1));
for idx = 1:size(values, 1)
testCases{idx} = this.createTestCase(values{idx, 1}, values{idx, 2});
end
end

function testCase = createTestCase(this, x, y)
%CREATETESTCASE Creates a function handle test case for a given test case

% Create TestCase object
testFcn = @() TestParameterizedSuite.checkResult(x, y);
setupFcn = [];
teardownFcn = [];

testCase = FunctionHandleTestCase(testFcn, setupFcn, teardownFcn);

% Set name and location of test from name of file
testCase.Name = this.makeTestName(x);
testCase.Location = this.Location;
end
function str = makeTestName(this, x) %#ok<MANU>
str = sprintf('test_sin_%.0fdeg', x);
str = genvarname(str);
end
end

methods (Static)
function checkResult(x, y)
assertElementsAlmostEqual(sind(x), y, 'absolute', 1e-9)
end
end
end
21 changes: 21 additions & 0 deletions tests/testIsTestSuiteSubclass.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
classdef testIsTestSuiteSubclass < TestCase
%testIsTestSuiteSubclass Unit tests for isTestSuiteSubclass

methods
function self = testIsTestSuiteSubclass(name)
self = self@TestCase(name);
end

function testTestSuite(~)
assertEqual(false, xunit.utils.isTestSuiteSubclass('TestSuite'));
end

function testSubclass(~)
assertEqual(true, xunit.utils.isTestSuiteSubclass('xunit.mocktests.C'));
end

function testNotASubclass(~)
assertEqual(false, xunit.utils.isTestSuiteSubclass('atan2'));
end
end
end
9 changes: 6 additions & 3 deletions tests/test_packageName.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@

function test_happyCase
suite = TestSuite.fromPackageName('xunit.mocktests');
assertEqual(numel(suite.TestComponents), 5);
assertEqual(numel(suite.TestComponents), 6);

theTestComponent = findTestComponent(suite, 'xunit.mocktests.subpkg');
assertEqual(numel(theTestComponent.TestComponents), 1);

theTestComponent = findTestComponent(suite, 'xunit.mocktests.A');
assertEqual(numel(theTestComponent.TestComponents), 2);

theTestComponent = findTestComponent(suite, 'xunit.mocktests.C');
assertEqual(numel(theTestComponent.TestComponents), 2);

theTestComponent = findTestComponent(suite, 'xunit.mocktests.FooTest');
assertEqual(numel(theTestComponent.TestComponents), 1);

Expand All @@ -33,8 +36,8 @@
% This is needed because meta.package.fromName() doesn't sort its list of
% functions, so the ordering isn't always stable.

components = [suite.TestComponents{:}];
index = find(strcmp(name, {components.Name}), 1);
componentNames = cellfun(@(x) x.Name, suite.TestComponents, 'UniformOutput', false);
index = find(strcmp(name, componentNames), 1);
assertTrue(~ isempty(index), ['Could not find test component for ' name]);

theTestComponent = suite.TestComponents{index};
Expand Down