MIPSUnit::MUnit
Current Version: 1.4.4 released December 2020
MIPSUnit::MUnit
is a JUnit
-based unit testing
framework for MIPS assembly. In particular, it is a Java interface
to the MARS
MIPS
simulator that helps users write JUnit
tests for their
assembly code.
MUnit
provides methods that
- set the initial machine state (registers and/or memory),
- instruct
MARS
to simulate the function under test, and - query the final state of the simulated machine.
JUnit
's Assert
methods to verify that the function under test behaved as expected.
Errors are reported using JUnit
's built-in error
reporting mechanism.
Example Workflow
Suppose you have assigned the following to your assembly language class:
in_range
that takes three parameters — value
,
min
,
and max
— and returns 1 (for true
) if min ≤ value ≤
max
and 0 (for false
) otherwise.
Be sure that your function is named in_range
(spelled correctly)
and declared in your code's .globl
section.
Begin by writing a JUnit
test class that describes the expected behavior of
in_range
:
Next, compile InRangeTest.java
.
javac -cp munit.jar InRangeTest.java
Finally, run the tests. Pass the name of the assembly file and the name of the class file on the command line. (Passing the name of the class file is unusual, but provides a way to run the tests without requiring the user to explicitly set up the runtime classpath.)
java -jar munit.jar in_range_correct.asm InRangeTest.class
Download these files and try it yourself:
Basics
Users use the Registers
enumeration to specify
registers. Including this line:
import static
edu.gvsu.mipsunit.munit.MUnit.Register.*;
allows users to refer to registers without further qualification.
Similarly, all methods are static
members of
MUnit
; thus, including
import static edu.gvsu.mipsunit.munit.MUnit.*;
allows users to call the key methods without further qualification. Our goal in using the enumeration and static methods was to minimize the amount of typing needed while still providing a reasonable level of compile-time error checking.
We expect that the vast majority of users will primarily use three
groups of methods from the MUnit
class:
...data
- Create a data label (i.e., a line in
the
.data
section) with the given values:Label d1 = wordData(1, 2, 4, 9, 16, 22, 18, 14);
Label c1 = byteData('a', 'b', 'c', 'd');
Label s1 = asciiData("Go, Jackets!");
set
- Set a register to a specific value:
set(t1, 16);
// equivalent toli $t1, 16
set(t1, d1);
// equivalent tola $t1, d1
run
- Run the function under test.
run("in_range", 10, 20, 30);
// extra parameters placed into $a0, $a1, $a2run("index_of", d1, 5, 10);
//d1
is a previously-createdLabel
get
- Get the value of the register:
get(v0);
get...
- Get values from memory:
getWord(d1, 3)
// get the word beginning 3 bytes pastLabel
d1
getByte(c1, 3)
// get the 3rd byte pastLabel
c1
getWords(d1, 0, 10)
// get the 10 words beginning atLabel
d1
getString(c1)
// get theString
beginning atLabel
c1
and ending with the next null character.
noOther...Modifications
- Verifies
that every memory address modified was retrieved by some
"
get...
" method (thereby verifying that the program didn't accidentally write to unexpected memory addresses).noOtherMemoryModifications
: Checks every memory location.noOtherStaticDataModifications
: Only checks the static data section of memory (typically addresses0x1000_0000
up to0x1000_4000
).noOtherNonStackModifications
: Checks both static data and the heap; but not the stack. (See the documentation for details on how stack size is determined.)
Advanced Techniques
Other JUnit
features
Users can take advantage of all JUnit
features. For
example, adding the @Before
method below sets register
v0
to a known value before each test. Setting
v0
to a value other than 0 or 1 allows us to verify that
the user is explicitly setting v0
to 0 (as opposed to
incorrectly assuming that v0
always contains 0 at the
beginning of a function).
@Before public void init_v0() { set(v0, 3343); }
Testing Memory
When testing a program that modifies memory, not only must we
verify that the memory locations we expect to modify contain the
correct values, but we must also verify that the program didn't
incorrectly modify other locations. Explicitly checking every possibly
memory location would take a long time. Therefore MUnit
tracks which memory locations are accessed by get...
methods and provides the noOther...Modifications
methods to verify that all modified memory locations were explicitly
retrieved. (The choice of the word "retrieved" is intentional. We
can verify that the test writer requested the value of each modified
memory location; but, we cannot verify that the test writer actually
verified the correctness of the returned value.)
The sample below demonstrates how to test an assembly function that reverses the contents of an array.
There are three methods that check different areas of data memory:
noOtherMemoryModifications
: Checks every memory location.noOtherStaticDataModifications
: Only checks the static data section of memory (typically addresses0x1000_0000
up to0x1000_4000
).noOtherNonStackModifications
: Checks both static data and the heap; but not the stack. (See the documentation for details on how stack size is determined.)
Download these files and try it yourself:
Why MUnit?
MIPSUnit::MUnit
is one of two related testing
frameworks. The other is the RSpec
-inspired
MSpec
.
Whereas MUnit
interacts directly with
MARS
, MSpec
produces an assembly file that
contains all the tests. This assembly file can run independently from
MSpec
.
MUnit
has two main advantages over
MSpec
:
- More students know Java and
JUnit
than Ruby andRSpec
; therefore, most students are likely to findMUnit
easier to use. MUnit
tests are better isolated from each other. BecauseMSpec
generates a single assembly language test driver, the tests share the same low-level runtime environment as the driver. This means:- It is not possible to
completely re-set the state of the CPU between tests: For example, a
program could conceivably modify the part of the
.data
section that will be input for another test. - The driver halts after reporting the first error. Storing the results of multiple tests would require keeping some registers and memory "off limits" to programs under test. We wanted to avoid limiting the programmer for sake of the testing environment; so we report the first error only.
- It is not possible to
completely re-set the state of the CPU between tests: For example, a
program could conceivably modify the part of the
MSpec
's main advantage over MUnit
is
expressiveness and flexibility. We like to use very thorough,
exhaustive tests when evaluating students' code. Preparing such
thorough tests in Java/JUnit
requires that we either
- write a lot of separate, but very similar
@Test
methods, or - write a single method that tests many cases.
MSpec
for a function to compute n
choose k
:
MSpec
-generated tests without knowing anything
about MSpec
. They don't even need to know that such a
program exists! In contrast, at minimum, students need to be taught
how to use munit.jar
to run MUnit
tests.
See the
MSpec
page for more advantages of MSpec
.
FAQ
What questions do you have?
Contact Info
You can e-mail me at last-name first-initial@gvsu.edu
,
or see my complete contact info here.
(C) 2014 Zachary Kurmas
Last updated: 7 December 2018