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.

    5. ca65.

      1. Test suite in ca65.

      2. Functions in ca65.

      3. Compiling test suite in ca65.

    6. xa65.

      1. Test suite in xa65.

      2. Functions in xa65.

      3. Compiling test suite in xa65.

  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

c64unit test framework supports also testing for programs written in C language. Please visit https://bitbucket.org/Commocore/c64unit-for-c to get c64unit for C. Test suite examples written in C are available here: https://bitbucket.org/Commocore/c64unit-for-c-examples.

c64unit has been tested on the following cross-assembler versions:

  • 64tass 1.53.1515 (Ubuntu 14.04 LTS, Windows 7 SP1)
  • DASM 2.20.11 (Windows 7 SP1)
  • Kick Assembler 4.13 (Windows 7 SP1)
  • ACME 0.96 (Windows 7 SP1)
  • ca65 2.13.3 (Windows 7 SP1)
  • xa65 2.3.5 (Ubuntu 14.04 LTS)

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

Test suite in ca65

Test suite in xa65

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 at $2000 memory location
.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 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.

    assertEqualToA #11, "green-feature-test"
    assertEqual #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
    -
        loadDataSetToA inputData ; load value to accumulator
        jsr addFiveFunction ; this function does: adc #5
        assertDataSetEqualToA 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 byte value under some address:

    loadDataSet inputData, register
    

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

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

    If you have to store data for lo-byte and hi-byte in two separate locations, just use this one instead:

    loadDataSetWordToLoHi inputData, registerLo, registerHi
    

    Note: In the examples above, register, registerLo and registerHi 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"
    assertCarryFlagNotSet "algorithm-feature-test failed: carry"
    assertDecimalFlagNotSet "algorithm-feature-test failed: decimal"
    assertNegativeFlagNotSet "algorithm-feature-test failed: negative"
    assertOverflowFlagNotSet "algorithm-feature-test failed: overflow"
    assertZeroFlagNotSet "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 "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 at $2000 memory location
c64unit_include set "package"
    include "cross-assemblers/dasm/core2000.asm"

5.2.2. Functions in DASM.

Check 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.

    assertEqualToA #11, "green-feature-test"
    assertEqual #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
            loadDataSetToA inputData ; load value to accumulator
            jsr addFiveFunction ; this function does: adc #5
            assertDataSetEqualToA 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 byte value under some address:

    loadDataSet inputData, register
    

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

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

    If you have to store data for lo-byte and hi-byte in two separate locations, just use this one instead:

    loadDataSetWordToLoHi inputData, registerLo, registerHi
    

    Note: In the examples above, register, registerLo and registerHi 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"
    assertCarryFlagNotSet "algorithm-feature-test failed: carry"
    assertDecimalFlagNotSet "algorithm-feature-test failed: decimal"
    assertNegativeFlagNotSet "algorithm-feature-test failed: negative"
    assertOverflowFlagNotSet "algorithm-feature-test failed: overflow"
    assertZeroFlagNotSet "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.

cd tests
dasm.exe "tests\test-suite.asm" -I..\vendor\c64unit -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 at $2000 memory location
.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 cross-assemblers/kick-assembler/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.

    assertEqualToA(11, "green-feature-test")
    assertEqual(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)
    !:
        loadDataSetToA(inputData) // load value to accumulator
        jsr addFiveFunction // this function does: adc #5
        assertDataSetEqualToA(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 byte value under some address:

    loadDataSet(inputData, register)
    

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

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

    If you have to store data for lo-byte and hi-byte in two separate locations, just use this one instead:

    loadDataSetWordToLoHi(inputData, registerLo, registerHi)
    

    Note: In the examples above, register, registerLo and registerHi 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")
    assertCarryFlagNotSet("algorithm-feature-test failed: carry")
    assertDecimalFlagNotSet("algorithm-feature-test failed: decimal")
    assertNegativeFlagNotSet("algorithm-feature-test failed: negative")
    assertOverflowFlagNotSet("algorithm-feature-test failed: overflow")
    assertZeroFlagNotSet("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 at $2000 memory location
!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 cross-assemblers/acme/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, .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.

    +assertEqualToA 11, .message1, .message1End
    +assertEqual 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
    -
        +loadDataSetToA .inputData ; load value to accumulator
        jsr addFiveFunction ; this function does: adc #5
        +assertDataSetEqualToA .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 byte value under some address:

    loadDataSet .inputData, .register
    

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

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

    If you have to store data for lo-byte and hi-byte in two separate locations, just use this one instead:

    +loadDataSetWordToLoHi .inputData, .registerLo, .registerHi
    

    Note: In the examples above, .register, .registerLo and .registerHi can be located whenever you want, including zero-page.

  6. Testing flags.

    +assertCarryFlagSet
    +assertDecimalFlagSet
    +assertNegativeFlagSet .message, .messageEnd
    +assertOverflowFlagSet .message, .messageEnd
    +assertZeroFlagSet
    +assertCarryFlagNotSet .message, .messageEnd
    +assertDecimalFlagNotSet .message, .messageEnd
    +assertNegativeFlagNotSet
    +assertOverflowFlagNotSet
    +assertZeroFlagNotSet
    
  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"

5.5. ca65 cross-assembler.

ca65 cross-assembler is the part of cc65, the cross-development package for 65(c)02 systems, including macro assembler, a C compiler, linker and several other tools.

This section covers ca65 cross-assembler support only.

c64unit test framework supports also testing for programs written in C language. Please visit https://bitbucket.org/Commocore/c64unit-for-c to get c64unit for C. Test suite examples written in C are available here: https://bitbucket.org/Commocore/c64unit-for-c-examples.

5.5.1. Test suite in ca65.

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

; Include c64unit at $2000 memory location
.include "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.5.2. Functions in ca65.

Check cross-assemblers/ca65/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.

    assertEqualToA #11, "green-feature-test"
    assertEqual #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 .scope 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
    :
        loadDataSetToA inputData ; load value to accumulator
        jsr addFiveFunction ; this function does: adc #5
        assertDataSetEqualToA 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 byte value under some address:

    loadDataSet inputData, register
    

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

    loadDataSetWord inputData, register
    
    inputData:
        .word 36700, 23505, 65535, 0, 16
    

    If you have to store data for lo-byte and hi-byte in two separate locations, just use this one instead:

    loadDataSetWordToLoHi inputData, registerLo, registerHi
    

    Note: In the examples above, register, registerLo and registerHi 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"
    assertCarryFlagNotSet "algorithm-feature-test failed: carry"
    assertDecimalFlagNotSet "algorithm-feature-test failed: decimal"
    assertNegativeFlagNotSet "algorithm-feature-test failed: negative"
    assertOverflowFlagNotSet "algorithm-feature-test failed: overflow"
    assertZeroFlagNotSet "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.5.3. Compiling test suite in ca65.

First of all, you have to configure memory layout. You have to create c64unit.cfg file which can be located in your test suite folder.

MEMORY {
    LOADADDR: start = $0188, size = 2, file = %O;
    RAM1: start = $0801, size = $5000;
    ROM1: start = $A000, size = $2000;
    ROM2: start = $E000, size = $2000;
}

SEGMENTS {
    LOADADDR: load = LOADADDR, type=ro;
    CODE: load = RAM1, type = rw;
    C64UNIT: load = RAM1, type = rw, start = $2000;
}

The most important part is to allocate the proper size of RAM1 memory location according to the core file you've set up in test suite (e.g. core2000.asm uses binary which is located from $2000 and takes another ~2.5kb).

Then, you have to set library absolute path to ca65 folder in c64unit using environment variable, e.g.:

SET CA65_INC=..\vendor\c64unit\cross-assemblers\ca65

or for *nix users:

export CA65_INC=../vendor/c64unit/cross-assemblers/ca65

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 CA65_INC=..\vendor\c64unit\cross-assemblers\ca65

With cl65 frontend for ca65, cc65, co65 and ld65, compiling your test suite can look like this:

cd F:\my\absolute\path\to\project\tests
cl65 -Oir -t c64 -C c64unit.cfg test-suite.asm -o build\test-suite.prg

To clean up, you can delete object file created during the compilation:

del test-suite.o

5.6. xa65 cross-assembler.

There are few constraints to set up testing environment for xa65 cross-assembler, so it's good to know about them before trying it out.

  1. xa65 struggles under Windows OS. The problem is that binary files may be trimmed in assembly code. It happens due to fact, that the occurrence of ^Z characters is treated as the end of file. As we speak, it doesn't work in v2.3.6 of xa65. Maintainer has been informed about this issue, so hopefully it will be fixed in future releases. Implementation for xa65 has been fully developed and tested on Linux, with xa65 v.2.3.5.

  2. As xa65 allows only to set include path for include files, not binaries, you have to organize your test suite exactly the same was as suggested in Organizing tests section. Only this way, test suite can access the vendor folder.

  3. Due to some unknown syntax error on compile time when using c64unit() macro name for the framework startup, to avoid this clash, function is renamed to c6unitRun() in comparison to other cross-assembler implementations. This clash occurs also when including c64unit from vendor folder, that's why folder name was renamed to vendor/c64unit-framework. To add c64unit to your project, please use the installation script from install/install-for-xa65 subfolder.

5.6.1. Test suite in xa65.

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

; Include _c64unit_ definitions (symbols and macros)
#define c64unit_include_definitions
#include "core2000.asm"

; Init
c64unitRun(1,$400)

; 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"

; Include _c64unit_ package
#define c64unit_include_package at $2000 memory location
#include "core2000.asm"

5.6.2. Functions in xa65.

Check cross-assemblers/xa65/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.

    assertEqualToA(11, "green-feature-test")
    assertEqual(11, register, "orange test should always return 11")
    

    Note: Custom message cannot be longer than 40 characters.

  4. Assertions for Data Sets.

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

    Hint: use .( and .) 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)
    loop
        loadDataSetToA(inputData) ; load value to accumulator
        jsr addFiveFunction ; this function does: adc #5
        assertDataSetEqualToA(expectedData, "orange-feature-test")
        isDataSetCompleted()
    bne loop
    rts
    
    inputData
        .byte 5, 10, 15, 20, 25, 30
    
    expectedData
        .byte 10, 15, 20, 25, 30, 35
    

    To pass a byte value under some address:

    loadDataSet(inputData, register)
    

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

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

    If you have to store data for lo-byte and hi-byte in two separate locations, just use this one instead:

    loadDataSetWordToLoHi(inputData, registerLo, registerHi)
    

    Note: In the examples above, register, registerLo and registerHi 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")
    assertCarryFlagNotSet("algorithm-feature-test failed: carry")
    assertDecimalFlagNotSet("algorithm-feature-test failed: decimal")
    assertNegativeFlagNotSet("algorithm-feature-test failed: negative")
    assertOverflowFlagNotSet("algorithm-feature-test failed: overflow")
    assertZeroFlagNotSet("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.6.3. Compiling test suite in xa65.

cd <your-test-suite-folder>
export XAINPUT=../vendor/c64unit-framework/cross-assemblers/xa65
rm -rf build/test-suite.prg
xa -v -M test-suite.asm -o build/test-suite.prg

6. License.

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

Released under the License.

All rights reserved.