Kopytko Unit Testing Framework
- App Structure
- Setup
- Running Unit Tests
- Kopytko Unit Test Philosophy
- Test Mocks
- Setup and Teardown
- Limitations
- API
- Example test app config and unit tests
- Migration from v1 to v2
The unit testing framework works on top of the Roku Unit Testing framework. There are some differences between those two frameworks.
App Structure
We believe tests should be close to the tested objects.
The expected structure of the app:
components
_mocks
MyService.mock.brs
_tests
MyComponent.test.brs
MyComponent.brs
MyComponent.xml
MyService.brs
The _tests
folders should be placed near to the tested entity. Each test suite gains extra powers:
- no need for xml files
- no need to define test suites functions in an array
- ability to import dependencies
- ability to mock dependencies automatically
- ability to mock dependencies manually
Setup
- Install framework as a dev dependency
npm install @dazn/kopytko-unit-testing-framework --save-dev
- Kopytko Unit Testing Framework uses Kopytko Packager to build apps.
If you don't use it yet, go to its docs and init a @kopytko app. Once done, setup test environment in your
.kopytkorc
file
{
"pluginDefinitions": {
"generate-tests": "/node_modules/@dazn/kopytko-unit-testing-framework/plugins/generate-tests"
},
"plugins": [
{ "name": "kopytko-copy-external-dependencies", "preEnvironmentPlugin": true }
],
"environments": {
"test": {
"plugins": ["generate-tests"]
}
}
}
Remark: You can use any name for the test environment, just be consistent.
- Setup test script in your
package.json
{
"scripts": {
"test": "ENV=test node ../scripts/test.js"
}
}
- [Temporary] To not force migration from v1 to v2 to be imidiate we introduced a bs_const flag (details in Migration part). The flag will be temporary for the depreciation period. In your manifest file please add bs_const:
{
bs_const: {
insertKopytkoUnitTestSuiteArgument: false,
}
}
Running Unit Tests
Simply
npm test
If you want to run unit tests of a specific unit, you can pass the file name as a default argument:
npm test -- MyTestableUnit
This is a shortcut for npm test -- --testFileName=MyTestableUnit
Kopytko Unit Test Philosophy
The unit tests can be split into multiple files and imported by the packager automatically. Let's consider the following example:
components
_tests
MyService_getData.test.brs
MyService_Main.test.brs
MyServiceTestSuite.test.brs
MyService.brs
MyService.brs
function MyService() as Object
prototype = {}
prototype.getData = function (arg as String) as Object
return { arg: arg }
end function
return prototype
end function
MyServiceTestSuite.test.brs
' @import /components/KopytkoTestSuite.brs from @dazn/kopytko-unit-testing-framework
function MyServiceTestSuite() as Object
ts = KopytkoTestSuite()
beforeAll(sub (_ts as Object)
' do something
end sub)
return ts
end function
MyService_Main.test.brs
function TestSuite__MyService_Main() as Object
ts = MyServiceTestSuite()
ts.name = "MyService - Main"
it("should create new instance of the service", function (_ts as Object) as String
return expect(MyService()).toBeValid()
end function)
return ts
end function
MyService_getData.test.brs
function TestSuite__MyService_getData() as Object
ts = MyServiceTestSuite()
ts.name = "MyService - getData"
it("should return some data", function (_ts as Object) as String
' Given
service = MyService()
expected = { arg: "abc" }
' When
result = service.getData("abc")
'Then
return expect(result).toEqual(expected)
end function)
return ts
end function
Such structure is understood and imported automatically by the packager.
Behind the scenes Kopytko Unit Testing Framework replaces the source/Main.brs file to run unit tests. Roku's Unit Testing Framework core file is automatically imported by Kopytko Packager via ROPM.
Test Mocks
Dependencies may be mocked by using @mock
annotation after dependencies import in the main test file:
' @mock pkg:/components/example/ExampleService.brs
Type of dependency will be automatically recognized and proper function or object mock will be automatically generated on the fly (during the build process). There are 3 different types:
- object that implements methods
- function
- node
In case that auto-generated mock doesn't suit our needs, manual mock may be created. It has to be created under _mocks
directory located next to the mocked file. Manual mock file has to be named after the mocked element with .mock.brs
postfix e.g. GlobalNode.mock.brs
for the GlobalNode.brs
element.
It's also possible to create a common config for the mock. To do so, a file named after the mocked element with .config.brs
postfix has to be added in the _mocks
directory. Such config will be automatically imported when a dependency is mocked by the @mock
annotation.
Example ExampleService.brs
:
function ExampleService(dependency as Object) as Object
prototype = {}
prototype.getData = function (arg as String) as Object
return { someKey: "someValue" }
end function
return prototype
end function
ExampleService.mock.brs
:
function ExampleService(dependency as Object) as Object
return Mock({
testComponent: m,
name: "ExampleService",
methods: {
getData: function (arg as Object) as Object
return m.getDataMock("getData", { arg: arg })
end function,
},
})
end function
In the unit tests the special field __mocks
will be created and configuration can be added:
m.__mocks.exampleService = {
getData: {
returnValue: 1,
},
}
The service can be used like a regular object:
service = ExampleService(dependency)
data = service.getData("/test")
When dependency is mocked (@mock
).
You can use our mockFunction
to set returned value of the mocked function. For example.
it("should return mocked function value", function (_ts as Object) as String
' Given
expected = 123
mockFunction("functionName").returnedValue(expected)
' When
result = functionReturningFunctionNameResult()
'Then
return expect(result).toEqual(expected)
end function)
Or you can check if mocked function was called properly
it("should call functionName once with argument a = 1", function (_ts as Object) as String
' When
result = functionReturningFunctionNameResult()
'Then
return [
expect("functionName").toHaveBeenCalledTimes(1),
expect("functionName").toHaveBeenCalledWith({ a: 1 }),
]
end function)
Here are listed all mockFunction
methods.
There are also plenty of examples here.
Calls to the methods or constructor can be inspected:
? mockFunction("ExampleService.getData").getCalls()[0].params.arg
? mockFunction("ExampleService").getConstructorCalls()[0].params.dependency
Setup and Teardown
Roku Unit Testing Framework provides the way to execute your custom code before/after every test suite.
However, to give more flexibility, Kopytko Unit Testing Framework overwrites setUp
and tearDown
properties of a test suite, so you shouldn't use them. Instead, add your function via beforeAll
or afterAll
methods of KopytkoTestSuite
.
KopytkoFrameworkTestSuite
already contains some additional code to prepare and clean a test suite from Kopytko ecosystem related stuff.
Notice that if you have test cases of a unit split into few files, every file creates a separate test suite, therefore all beforeAll
and afterAll
callbacks will be executed once per a file.
KopytkoTestSuite
provides additional possibility to run custom code before/after every test suite via setBeforeEach
and setAfterEach
methods.
Functions passed into all these methods and arrays should have just one ts
argument which is a test suite.
Limitations
- The Framework was not tested with the annotations
API
Example test app config and unit tests
Go to /example directory
Migration from v1 to v2
Version 2 introduces new shorthand functions and because of that, we were able to remove the test suite object argument from the test case function.
Now if you want to get the ts
(test suite) object, you can get it by calling the ts()
function in a test case.
In order to not make trouble for projects that already use v1, we introduced a bs_const flag insertKopytkoUnitTestSuiteArgument. So if you don't want to change the current test cases implementation add it to your manifest with the value set to true.
When you want to use our new shorthand methods and you don't need a test suite object argument, as you don't use it, set this flag to false.
IMPORTANT: This flag is only temporary and will be removed in the future. The desired solution is to not use the test suite argument.