HTTPS SSH

c64unit

The ultimate unit test framework for Commodore 64.

c64unit

Contents

  1. About.

  2. Installation.

  3. Organizing tests.

  4. Creating your first test suite.

  5. Cross-assemblers.

    1. 64tass.

      1. Test suite in 64tass.

      2. Functions in 64tass.

      3. Compiling test suite in 64tass.

    2. DASM.

      1. Test suite in DASM.

      2. Functions in DASM.

      3. Compiling test suite in DASM.

    3. Kick Assembler.

      1. Test suite in Kick Assembler.

      2. Functions in Kick Assembler.

      3. Compiling test suite in Kick Assembler.

    4. ACME.

      1. Test suite in ACME.

      2. Functions in ACME.

      3. Compiling test suite in ACME.

  6. License.

1. About.

Potentially, c64unit test framework allows you to test your code within any cross-assembler of your choice. The package itself is a compiled binary, which can be accessed by any other cross-assembler. And with few handy macros in a toolbox, testing your code is just simple!

To see c64unit in action, please visit examples repository: https://bitbucket.org/Commocore/c64unit-examples.

Features:

  • uses macros so writing tests requires only few lines of test code, and hides internal framework's mechanisms
  • no variables used on zero-page, so code can be tested freely
  • most common cross-assemblers supported so far, and opened to extension
  • you can examine your function against different conditions - use Data Sets to initialize any number of conditions on fly like zero page addresses, other memory locations or just by passing A, X, Y registers
  • forget about writing assertions over and over again for each case, Data Sets allows not only to initialize, but also to assert multiple set of data dynamically, up to 256 byte values per table, or 128 word values per table
  • you can mock methods, which means you can test your code in isolation, create stubs, skip loading data from disk and much more
  • you can assert not only values or memory addresses, but also flag statuses like carry flag, negative flag and so on

2. Installation.

To add c64unit into your project, just copy install/c64unit-dependency.bat (Windows), or install/c64unit-dependency.sh (Linux, Mac) script file from this repository to your main project folder. Execution of the script file creates vendor/c64unit folder and clones this repository using git version control system. It's recommended to list vendor folder in .gitignore file.

3. Organizing tests.

Folder structure can vary, and it's up to you to organize your tests the way you like. To give you some inspiration though, take this example:

.
+-- src
    +-- (your code here)
+-- tests
    +-- build (this folder can be added to .gitignore)
        +-- test-suite.prg (compiled test suite output file)
    +-- test-cases
        +-- functionality-1
            +-- green-feature-test.asm
            +-- orange-feature-test.asm
            +-- ...
        +-- functionality-2
            +-- algorithm-feature-test.asm
            +-- ...
    +-- test-suite.asm
+-- vendor
    +-- c64unit
        +-- (content of this package)
+-- c64unit-dependency.sh

4. Creating your first test suite.

You can organize your tests in a test suite focused on a particular cycle of the project or its scope, etc. You can share tests across different test suites, or you can have just one test suite - it's up to you.

To create a test suite file, first of all you have to check where you can allocate c64unit in memory, most often simply after the test suite and all function(s) you want to test. Once you have found the free memory, you need to select a c64unit core package which corresponds to that empty area.

e.g. if you have free memory from $2000 on, you can include core2000.asm package, or higher like core3000.asm.

For example, your memory can be organized this way:

  1. Start of test suite (at $0801).
  2. Included functions you want to test.
  3. Test cases for these functions.
  4. c64unit package (at $2000).

Depending on a cross-assembler used, the implementations vary, so please see below for an example for a corresponding cross-assembler to see how to create a test suite file.

Test suite in 64tass

Test suite in DASM

Test suite in Kick Assembler

Test suite in ACME

5. Cross-assemblers.

c64unit itself is a compiled package. With this idea in mind, it can be widely used by any cross-assembler. The only thing which needs to be implemented is a "bridge" between the binary package, and a particular cross-assembler. This can be fulfilled with macros, but not necessary. However, macros allows to have very handy set of functions, which makes c64unit even more easier and quicker to use. This chapter covers all implementations supported by the framework so far.

5.1. 64tass cross-assembler.

5.1.1. Test suite in 64tass.

Your test-suite.asm file can look just this way:

; Include c64unit
.include "../vendor/c64unit/cross-assemblers/64tass/core2000.asm"

; Init c64unit
c64unit

; Examine test cases
examineTest testGreenFeature
examineTest testOrangeFeature
examineTest testAlgorithmFeature

; If this point is reached, there were no assertion fails
c64unitExit

; Include domain logic, i.e. classes, methods and tables
.include "../src/includes/green-function.asm"
.include "../src/includes/orange-function.asm"
.include "../src/includes/algorithms/algorithm-function.asm"

; Test cases
.include "test-cases/functionality-1/green-feature-test.asm"
.include "test-cases/functionality-1/orange-feature-test.asm"
.include "test-cases/functionality-2/algorithm-feature-test.asm"

5.1.2. Functions in 64tass.

Check c64unit/cross-assemblers/64tass/macros.asm for reference.

To see c64unit in action, please visit examples repository: https://bitbucket.org/Commocore/c64unit-examples.

  1. Simple assertions against processor registers.

    assertEqualToA #11
    assertEqualToX myRegister
    assertEqualToY #$fa
    assertNotEqualToA #0
    
  2. Assertions against address memory.

    assertEqual #11, myRegister
    assertGreater #11, myRegister
    assertGreaterOrEqual #11, $7000
    assertLess #11, myRegister
    assertNotEqual #0, myRegister
    assertMemoryEqual expectedTable, actualTable, 1024
    

    Note: Address memory can be a zero-page, or 16-bit address, it really doesn't matter for c64unit.

    Assertions for 16-bit values are very similar:

    assertWordEqual 32557, myRegister, "test-method.asm"
    

    Note: Expected value is a 16-bit immediate word, and myRegister word can be located anywhere, including zero-page. However, you can also assert against absolute value using assertAbsoluteWordEqual.

  3. Displaying custom messages if assertion fails.

    Custom messages can be very handy to point to the test case file if you have thousands of tests, or to describe the expected behaviour.

    assertEqualsToA #11, "green-feature-test"
    assertEquals #11, register, "orange test should always return 11"
    

    Note: Custom message cannot be longer than 40 characters.

  4. Assertions for Data Sets.

    prepareDataSetLength 6
    -
        jsr myFunctionToTest
        assertDataSetGreater expectedData, "orange-feature-test"
        isDataSetCompleted
    bne -
    rts
    
    expectedData
        .byte 5, 10, 15, 20, 25, 30
    

    Hint: use .proc to encapsulate your test labels, so you can reuse expectedData more than once in test suite.

    Assertion of data set for 16-bit values is very similar:

    assertDataSetWordEqual expectedData, result, "orange-feature-test"
    
    ...
    
    expectedData
        .word 12940, 1945, 0, 41, 17, 46054
    
  5. Using Data Sets to pass values to tested function.

    prepareDataSetLength 6
    -
        getDataSet inputData
        jsr addFiveFunction ; this function does: adc #5
        assertDataSetEqual expectedData, "orange-feature-test"
        isDataSetCompleted
    bne -
    rts
    
    inputData
        .byte 5, 10, 15, 20, 25, 30
    
    expectedData
        .byte 10, 15, 20, 25, 30, 35
    

    To pass a word value, the only difference is that you have to provide a 16-bit register where data will be stored:

    getDataSetWord inputData, register
    
    inputData
        .word 36700, 23505, 65535, 0, 16
    

    Note: register can be located whenever you want, including zero-page.

  6. Testing flags.

    assertCarryFlagSet "algorithm-feature-test failed: carry"
    assertDecimalFlagSet "algorithm-feature-test failed: decimal"
    assertNegativeFlagSet "algorithm-feature-test failed: negative"
    assertOverflowFlagSet "algorithm-feature-test failed: overflow"
    assertZeroFlagSet "algorithm-feature-test failed: zero"
    
  7. Mocking methods.

    mockMethod loadDataFromDisk, loadDataFromDiskMock
    
    ...
    
    loadDataFromDiskMock
        ; your logic here to set memory instead of reading data from disk
    rts
    

    This way you can test your function in isolation, unit way for real!

    Note: all method mocks are reset when next test is executed.

5.1.3. Compiling test suite in 64tass.

64tass.exe -a "tests\test-suite.asm" -o "tests\build\test-suite.prg"

5.2. DASM cross-assembler.

5.2.1. Test suite in DASM.

As all segments needs to have ascending order for program counter (PC), test suite for DASM vary from other implementations. You have to set c64unit_include value to "definitions" for the first include of core file, then, at the end of test suite you have to set another one, but for "package" value. Note that c64unit_include label needs to be set on the left, without any indentation.

Your test-suite.asm file can look just this way:

    processor 6502

    ; Include c64unit definitions (symbols and macros)
c64unit_include set "definitions"
    include "../vendor/c64unit/cross-assemblers/dasm/core2000.asm"

    ; Init
    c64unit 1, $0400

    ; Examine test cases
    examineTest testGreenFeature
    examineTest testOrangeFeature
    examineTest testAlgorithmFeature

    ; If this point is reached, there were no assertion fails
    c64unitExit

    ; Include domain logic, i.e. classes, methods and tables
    include "../src/includes/green-function.asm"
    include "../src/includes/orange-function.asm"
    include "../src/includes/algorithms/algorithm-function.asm"

    ; Test suite with all test cases
    include "test-cases/functionality-1/green-feature-test.asm"
    include "test-cases/functionality-1/orange-feature-test.asm"
    include "test-cases/functionality-2/algorithm-feature-test.asm"

    ; Include c64unit package
c64unit_include set "package"
    include "../vendor/c64unit/cross-assemblers/dasm/core2000.asm"

5.2.2. Functions in DASM.

Check c64unit/cross-assemblers/dasm/macros.asm for reference.

To see c64unit in action, please visit examples repository: https://bitbucket.org/Commocore/c64unit-examples.

  1. Simple assertions against processor registers.

    assertEqualToA #11, ""
    assertEqualToX myRegister, ""
    assertEqualToY #$fa, ""
    assertNotEqualToA #0, ""
    

    Note: Second parameter for custom message is mandatory, so you have to provide at least an empty string

  2. Assertions against address memory.

    assertEqual #11, myRegister, ""
    assertGreater #11, myRegister, ""
    assertGreaterOrEqual #11, $7000, ""
    assertLess #11, myRegister, ""
    assertNotEqual #0, myRegister, ""
    assertMemoryEqual expectedTable, actualTable, 1024, ""
    

    Note: Address memory can be a zero-page, or 16-bit address, it really doesn't matter for c64unit.

    Assertions for 16-bit values are very similar:

    assertWordEqual 32557, myRegister, "test-method.asm"
    

    Note: Expected value is a 16-bit immediate word, and myRegister word can be located anywhere, including zero-page. However, you can also assert against absolute value using assertAbsoluteWordEqual.

  3. Displaying custom messages if assertion fails.

    Custom messages can be very handy to point to the test case file if you have thousands of tests, or to describe the expected behaviour.

    assertEqualsToA #11, "green-feature-test"
    assertEquals #11, register, "orange test should always return 11"
    

    Note: Custom message cannot be longer than 40 characters.

  4. Assertions for Data Sets.

        prepareDataSetLength 6
    .1
            jsr myFunctionToTest
            assertDataSetGreater expectedData, "orange-feature-test"
            isDataSetCompleted
        bne .1
        rts
    
    expectedData
        .byte 5, 10, 15, 20, 25, 30
    

    Assertion of data set for 16-bit values is very similar:

    assertDataSetWordEqual expectedData, result, "orange-feature-test"
    
    ...
    
    expectedData
        .word 12940, 1945, 0, 41, 17, 46054
    
  5. Using Data Sets to pass values to tested function.

        prepareDataSetLength 6
    .1
            getDataSet inputData
            jsr addFiveFunction ; this function does: adc #5
            assertDataSetEqual expectedData, "orange-feature-test"
            isDataSetCompleted
        bne .1
        rts
    
    inputData
        .byte 5, 10, 15, 20, 25, 30
    
    expectedData
        .byte 10, 15, 20, 25, 30, 35
    

    To pass a word value, the only difference is that you have to provide a 16-bit register where data will be stored:

    getDataSetWord inputData, register
    
    inputData
        .word 36700, 23505, 65535, 0, 16
    

    Note: register can be located whenever you want, including zero-page.

  6. Testing flags.

    assertCarryFlagSet "algorithm-feature-test failed: carry"
    assertDecimalFlagSet "algorithm-feature-test failed: decimal"
    assertNegativeFlagSet "algorithm-feature-test failed: negative"
    assertOverflowFlagSet "algorithm-feature-test failed: overflow"
    assertZeroFlagSet "algorithm-feature-test failed: zero"
    
  7. Mocking methods.

        mockMethod loadDataFromDisk, loadDataFromDiskMock
    
        ...
    
    loadDataFromDiskMock:
        ; your logic here to set memory instead of reading data from disk
        rts
    

    This way you can test your function in isolation, unit way for real!

    Note: all method mocks are reset when next test is executed.

5.2.3. Compiling test suite in DASM.

You have to point include path to c64unit's DASM cross-assembler folder within -I option.

dasm.exe "tests\test-suite.asm" -Ivendor\c64unit\cross-assemblers\dasm -o"tests\build\test-suite.prg"

5.3. Kick Assembler cross-assembler.

5.3.1. Test suite in Kick Assembler.

Your test-suite.asm file can look just this way:

// Include c64unit
.import source "../vendor/c64unit/cross-assemblers/kick-assembler/core2000.asm"

// Init
c64unit(1, 0)

// Examine test cases
examineTest(testGreenFeature)
examineTest(testOrangeFeature)
examineTest(testAlgorithmFeature)

// If this point is reached, there were no assertion fails
c64unitExit()

// Include domain logic, i.e. classes, methods and tables
.import source "../src/includes/green-function.asm"
.import source "../src/includes/orange-function.asm"
.import source "../src/includes/algorithms/algorithm-function.asm"

// Test cases
.import source "test-cases/functionality-1/green-feature-test.asm"
.import source "test-cases/functionality-1/orange-feature-test.asm"
.import source "test-cases/functionality-2/algorithm-feature-test.asm"

5.3.2. Functions in Kick Assembler.

Check c64unit/cross-assemblers/kick-assembler/macros.asm for reference.

To see c64unit in action, please visit examples repository: https://bitbucket.org/Commocore/c64unit-examples.

In case of Kick Assembler, functions can pass immediate expected value, or absolute expected value.

Calling assertEqualToA(10, "") will pass #10.

Calling assertAbsoluteEqualToA(myRegister, "") will pass value under myRegister address (zero-page, or word).

  1. Simple assertions against processor registers.

    assertEqualToA(11, "")
    assertAbsoluteEqualToX(myRegister, "")
    assertEqualToY($fa, "")
    assertNotEqualToA(0, "")
    

    Note: Second parameter for custom message is mandatory, so you have to provide at least an empty string.

  2. Assertions against address memory.

    assertEqual(11, myRegister, "")
    assertGreater(11, myRegister, "")
    assertGreaterOrEqual(11, $7000, "")
    assertLess(11, myRegister, "")
    assertNotEqual(0, myRegister, "")
    assertMemoryEqual(expectedTable, actualTable, 1024, "")
    

    Note: Address memory can be a zero-page, or 16-bit address, it really doesn't matter for c64unit.

    Assertions for 16-bit values are very similar:

    assertWordEqual(32557, myRegister, "test-method.asm")
    

    Note: Expected value is a 16-bit immediate word, and myRegister word can be located anywhere, including zero-page. However, you can also assert against absolute value using assertAbsoluteWordEqual().

  3. Displaying custom messages if assertion fails.

    Custom messages can be very handy to point to the test case file if you have thousands of tests, or to describe the expected behaviour.

    assertEqualsToA(11, "green-feature-test")
    assertEquals(11, register, "orange test should always return 11")
    

    Note: Custom message cannot be longer than 40 characters.

  4. Assertions for Data Sets.

    prepareDataSetLength(6)
    !:
        jsr myFunctionToTest
        assertDataSetGreater(expectedData, "orange-feature-test")
        isDataSetCompleted()
    bne !-
    rts
    
    expectedData:
        .byte 5, 10, 15, 20, 25, 30
    

    Hint: use curly braces to encapsulate your test labels, so you can reuse expectedData more than once in test suite.

    Assertion of data set for 16-bit values is very similar:

    assertDataSetWordEqual(expectedData, result, "orange-feature-test")
    
    ...
    
    expectedData:
        .word 12940, 1945, 0, 41, 17, 46054
    
  5. Using Data Sets to pass values to tested function.

    prepareDataSetLength(6)
    !:
        getDataSet(inputData)
        jsr addFiveFunction // this function does: adc #5
        assertDataSetEqual(expectedData, "orange-feature-test")
        isDataSetCompleted()
    bne !-
    rts
    
    inputData:
        .byte 5, 10, 15, 20, 25, 30
    
    expectedData:
        .byte 10, 15, 20, 25, 30, 35
    

    To pass a word value, the only difference is that you have to provide a 16-bit register where data will be stored:

    getDataSetWord(inputData, register)
    
    inputData:
        .word 36700, 23505, 65535, 0, 16
    

    Note: register can be located whenever you want, including zero-page.

  6. Testing flags.

    assertCarryFlagSet("algorithm-feature-test failed: carry")
    assertDecimalFlagSet("algorithm-feature-test failed: decimal")
    assertNegativeFlagSet("algorithm-feature-test failed: negative")
    assertOverflowFlagSet("algorithm-feature-test failed: overflow")
    assertZeroFlagSet("algorithm-feature-test failed: zero")
    
  7. Mocking methods.

    mockMethod(loadDataFromDisk, loadDataFromDiskMock)
    
    ...
    
    loadDataFromDiskMock:
        ; your logic here to set memory instead of reading data from disk
    rts
    

    This way you can test your function in isolation, unit way for real!

    Note: all method mocks are reset when next test is executed.

5.3.3. Compiling test suite in Kick Assembler.

java -jar KickAss.jar tests\test-suite.asm -o tests\build\test-suite.prg

5.4. ACME cross-assembler.

5.4.1. Test suite in ACME.

Your test-suite.asm file can look just this way:

!zone testsuite
!cpu 6510

; Include c64unit
!src "../vendor/c64unit/cross-assemblers/acme/core2000.asm"

; Init
+c64unit

; Examine test cases
+examineTest testGreenFeature
+examineTest testOrangeFeature
+examineTest testAlgorithmFeature

; If this point is reached, there were no assertion fails
+c64unitExit

; Include domain logic, i.e. classes, methods and tables
!src "../src/includes/green-function.asm"
!src "../src/includes/orange-function.asm"
!src "../src/includes/algorithms/algorithm-function.asm"

; Test cases
!src "test-cases/functionality-1/green-feature-test.asm"
!src "test-cases/functionality-1/orange-feature-test.asm"
!src "test-cases/functionality-2/algorithm-feature-test.asm"

5.4.2. Functions in ACME.

Check c64unit/cross-assemblers/acme/macros.asm for reference.

To see c64unit in action, please visit examples repository: https://bitbucket.org/Commocore/c64unit-examples.

In case of ACME, functions can pass immediate expected value, or absolute expected value.

Calling +assertEqualToA 10 will pass #10.

Calling +assertAbsoluteEqualToA .myRegister will pass value under .myRegister address (zero-page, or word).

  1. Simple assertions against processor registers.

    +assertEqualToA 11
    +assertEqualToX .myRegister
    +assertEqualToY #$fa
    +assertNotEqualToA 0
    
  2. Assertions against address memory.

    +assertEqual 11, .myRegister
    +assertGreater 11, .myRegister
    +assertGreaterOrEqual 11, $7000
    +assertLess 11, .myRegister
    +assertNotEqual 0, .myRegister
    +assertMemoryEqual .expectedTable, .actualTable, 1024
    

    Note: Address memory can be a zero-page, or 16-bit address, it really doesn't matter for c64unit.

    Note also, that all expected values are immediate ones, but if you need to assert against absolute value, you have to use macro equivalent for absolute values:

    +assertAbsoluteEqual .myAddressWithExpectedValue, .myRegister
    

    Assertions for 16-bit values are very similar:

    +assertWordEqual 32557, .myRegister, .message, .messageEnd
    

    Note: Expected value is a 16-bit immediate word, and .myRegister word can be located anywhere, including zero-page. However, you can also assert against absolute value using +assertAbsoluteWordEqual.

  3. Displaying custom messages if assertion fails.

    Custom messages can be very handy to point to the test case file if you have thousands of tests, or to describe the expected behaviour.

    +assertEqualsToA 11, .message1, .message1End
    +assertEquals 11, .register, .message2, .message2End
    
    ...
    
    .message1
        !scr "green-feature-test"
    .message1End
    
    .message2
        !scr "orange test should always return 11"
    .message2End
    

    Unfortunately, ACME cross-assembler doesn't allow to pass string messages directly into macro, so you have to point to local labels below, and also provide the second label where message is finishing. Use !zone for each test to keep all local labels in isolation, so you can reuse .message label across the whole test suite.

    Note: Custom message cannot be longer than 40 characters.

  4. Assertions for Data Sets.

    +prepareDataSetLength 6
    -
        jsr myFunctionToTest
        +assertDataSetGreater .expectedData, .message, .messageEnd
        +isDataSetCompleted
    bne -
    rts
    
    .expectedData
        !byte 5, 10, 15, 20, 25, 30
    
    .message
        !scr "orange-feature-test"
    .messageEnd
    

    Hint: use !zone to encapsulate your test labels, so you can reuse .expectedData and .message more than once in test suite.

    Note: anonymous labels (-, +) are available since ACME v0.91.

    Assertion of data set for 16-bit values is very similar:

    +assertDataSetWordEqual .expectedData, result, .message, .messageEnd
    
    ...
    
    .expectedData
        !word 12940, 1945, 0, 41, 17, 46054
    
    .message
        !scr "orange-feature-test"
    .messageEnd
    
  5. Using Data Sets to pass values to tested function.

    +prepareDataSetLength 6
    -
        +getDataSet .inputData
        jsr addFiveFunction ; this function does: adc #5
        +assertDataSetEqual .expectedData, .message, .messageEnd
        +isDataSetCompleted
    bne -
    rts
    
    .inputData
        !byte 5, 10, 15, 20, 25, 30
    
    .expectedData
        !byte 10, 15, 20, 25, 30, 35
    
    .message
        !scr "orange-feature-test"
    .messageEnd
    

    To pass a word value, the only difference is that you have to provide a 16-bit register where data will be stored:

    +getDataSetWord .inputData, register
    
    .inputData
        !word 36700, 23505, 65535, 0, 16
    

    Note: register can be located whenever you want, including zero-page.

  6. Testing flags.

    +assertCarryFlagSet
    +assertDecimalFlagSet
    +assertNegativeFlagSet .message, .messageEnd
    +assertOverflowFlagSet .message, .messageEnd
    +assertZeroFlagSet
    
  7. Mocking methods.

    +mockMethod loadDataFromDisk, .loadDataFromDiskMock
    
    ...
    
    .loadDataFromDiskMock
        ; your logic here to set memory instead of reading data from disk
    rts
    

    This way you can test your function in isolation, unit way for real!

    Note: all method mocks are reset when next test is executed.

5.4.3. Compiling test suite in ACME.

First of all, you have to set library absolute path to c64unit using environment variable, e.g.:

SET ACME=F:\my\absolute\path\to\project\vendor\c64unit

or for *nix users:

export ACME=/my/absolute/path/to/project/vendor/c64unit/

Hint: If you're using Notepad++ with Execute plugin, you can set this value on the fly (variable won't be accessible out of Notepad++ execution), e.g.:

ENV_SET ACME=F:\my\absolute\path\to\project\vendor\c64unit

Then, you have to specify the output file by using !to command in the source of test suite, or by compiling the source with format option (-f) set to write file as PRG type, e.g.:

acme.exe -o "build\test-suite.prg" -f cbm "tests\test-suite.asm"

6. License.

Copyright © 2017, Bartosz Żołyński, Commocore.

Released under the License.

All rights reserved.