In this MV Tip we will be discussing using the %UnitTest class.
A unit test helps in code development by testing for the validity of a section of code. As developers improve and build on code they can use unit testing to verify that code changes did not affect the expected result with a given set of inputs. In addition to code verification, unit testing provides automatic regression testing. By running developed code through unit tests we check to see if the changes made "broke" the section of code alerting the developer before it is put into production.
The %UnitTest.TestCase class give us several test methods as seen below. When we use one of the Assert* methods it will either return true or false. For example, if we were to use the AssertStatusOkViaMacro the test would pass if the the Status value is 1. And if the Status value is 0 the test would fail. In the examples below we will see this in action. We would look at the first four Assert* methods for this Tip.
AssertEqualsViaMacro — Returns true if expressions are equal.
AssertNotEqualsViaMacro — Returns true if expressions are not equal.
AssertStatusOKViaMacro — Returns true if the returned status code is 1.
AssertStatusNotOKViaMacro — Returns true if the returned status code is 0.
AssertTrueViaMacro — Returns true if the expression is true.
AssertNotTrueViaMacro — Returns true if the expression is not true.
AssertFilesSameViaMacro — Returns true if two files are identical.
Here are some simple examples of code that we will run %UnitTest code against to verify their validity.
The first examples show how we can test functions and subroutines. We have defined a function called MyAddFunction and a subroutine called MyAddSub:
MyAddFunction0001 FUNCTION MyAddFunction(x,y)0002 RETURN (x+y) MyAddSub0001 SUBROUTINE MyAddSub(x,y,sum)0002 sum=x+y0003 RETURNBoth our function and subroutine take two inputs, x and y, and add those inputs. And both return the result of that operation.
For unit testing the goal is to provide known inputs and have an expected result when the piece of code is finished operating on the inputs. If, with the given inputs, our result is not the expected result the test has failed. Likewise, if, with the same set of inputs, the code returns the result we expected, the code has passed the test. So lets demonstrate how to test these two pieces of code.
Using Studio we need to create a class that extends the %UnitTest.TestCase class:
Class MyTests.SimpleExamples Extends %UnitTest.TestCase{Method TestAddSub() [ Language = mvbasic ]{ CALL MyAddSub(2,2,sum) value = sum @me->AssertEqualsViaMacro("",value,4, "Test Add(2,2)=4") @me->AssertNotEqualsViaMacro("",value,5,"Test Add(2,2)'=5")}Method TestAddFunction() [ Language = mvbasic ]{ DEFFUN MyAddFunction(a,b) value=MyAddFunction(2,2) @me->AssertEqualsViaMacro("",value,4,"Test Add(2,2)=4") @me->AssertNotEqualsViaMacro("",value,5,"Test Add(2,2)'5")}}Our first method in the MyTest.SimpleExamples class is TestAddSub() which will perform unit testing on the subroutine MyAddSub (as displayed above). We are evaluating whether or not providing the values of 2 and 2 will result in a value of 4 and whether or not providing the values of 2 and 2 will result in the value of 5.
Our second method in the MyTest.SimpleExapmles class is TestAddFunc() which will perform unit testing on the function MyAddFunction (as displayed above). Again, as described for the MyAddSub unit test, with the values of 2 and 2 we expect a value of 4 to return from the function. Also we expect a result of 5 to be incorrect.
So we now have a subroutine and a function and Unit Test class to test those pieces of code. Now lets unit test our code!
To begin we first need to do some set up:
1) Open Caché Terminal in the namespace/account that contains your unit tests; in this example I use 'MVTIPS'.
2) Set the value of the ^UnitTestRoot global to a directory that will contain your exported unit test classes. For example:
I will store my unit tests in "c:\unittest\mytests" in the filesystem. I will set my ^UnitTestRoot global to equal "c:\unittests":
MVTIPS:[set ^UnitTestRoot="c:\unittests"
3) Export our unit test class (MyTests.SimpleExamples) to "c:\unittests\mytests"
-In Caché Studio, click Tools —> Export.
-Click Add and MyTests.SimpleExamples.cls.
-Click Browse and navigate to your unit test directory; in this example, C:\unittests\mytests\.
-Enter a name for the new XML file; in this example, Tests.xml.
-Click OK.
-Caché creates Tests.xml file containing the test class in the directory C:\unittests\mytests\.
4. Run our unit tests and review output in Caché Terminal:
MVTIPS:[do ##class(%UnitTest.Manager).RunTest("mytests")
This command is instructing the unit test manager to import, compile, and run all the tests (all the xml files) in "c:\unittests\mytests". "c:\unittests" comes from ^UnitTestRoot and the "\mytests" subdirectory is provided as an argument in the RunTest method.
Here is the output we should see from Caché terminal:
MVTIPS:[d ##class(%UnitTest.Manager).RunTest("mytests") ===============================================================================Directory: C:\unittests\mytests\===============================================================================mytests begins ...Load of directory started on 05/14/2012 14:31:36 '*.xml;*.XML' Loading file C:\unittests\mytests\Tests.xml as xmlImported class: MyTests.SimpleExamples Compilation started on 05/14/2012 14:31:36 with qualifiers 'ck/nodisplay/display=log/display=error'Compiling class MyTests.SimpleExamplesCompiling routine MyTests.SimpleExamples.1Compilation finished successfully in 0.134s. Load finished successfully. MyTests.SimpleExamples begins ... TestAddFunc() begins ... AssertEquals:Test Add(2,2)=4 (passed) AssertNotEquals:Test Add(2,2)'5 (passed) LogMessage:Duration of execution: .007441 sec. TestAddFunc passed TestAddSub() begins ... AssertEquals:Test Add(2,2)=4 (passed) AssertNotEquals:Test Add(2,2)'=5 (passed) LogMessage:Duration of execution: .002201 sec. TestAddSub passed MyTests.SimpleExamples passedmytests passed Use the following URL to view the result:http://localhost:57772/csp/samples/%25UnitTest.Report.cls?NS=MVTIPS&INDEX=1And we can see from the output above that both our tests passed for 2+2.
Our next example will work with a class. We can use unit tests on objects too! (Information on how to use objects in MVBasic please review this previous MV Tip:
https://sites.google.com/site/intersystemsmv/home/a-cache-of-tips/classesandmvbasic)
Here is a class created using MVBasic syntax:
Class MyDemo.PERSON Extends (%Persistent, %MV.Adaptor) [ Inheritance = right, Language = mvbasic ]{Property Name As %String(MVWIDTH = 25);Property Age As %String(MVJUSTIFICATION = "R");Property Hair As %String(VALUELIST = ",blond,red,brunette");ClassMethod Create(fname, lname, age, hair) As MyDemo.PERSON{ fullname = lname:",":fname * * Make a new object PER = "MyDemo.PERSON"->%New() * Set properties of the object * PER->Name = fullname PER->Age = age PER->Hair= hair RETURN PER *Return the PER object to the caller}}And below is our Unit Test for this class. When we provide the set of inputs for the MyDemo.PERSON->Create method we know the expected results. Any other results are incorrect.
Class MyTests.SimpleExamples1 Extends %UnitTest.TestCase{Method TestCreatePerson() [ Language = mvbasic ]{ myfname="Jane" mylname="Smith" myage=66 myhaircolor="blond" *These are the inputs for the MyDemo.PERSON->Create method.
*With these inputs we are expecting a specific result. fullname="Smith,Jane" *Smith,Jane is the expected Name value for the object we will create below.
*If any other value is returned from the MyDemo.PERSON->Create method this test will fail. newperson="MyDemo.PERSON"->Create(myfname,mylname,myage,myhaircolor) @me->AssertEqualsViaMacro("",newperson->Name,fullname, "Name Assignment") *Check to see that the newperson->Name value equals our expected Name value of Smith,Jane @me->AssertStatusOKViaMacro("",newperson->%Save(),"Person Created OK") *Check to see that we were able to save the newperson object OK}}Here are two examples of how to get our unit tests to fail:
1) In the MyDemo.PERSON->Create method we could change the "fullname = lname:",":fname" line to "fullname = lname:"-":fname". This would cause the first test to fail ( @me->AssertEqualsViaMacro("",newperson->Name,fullname, "Name Assignment") ) because our full name is now Smith-Jane when it should be Smith,Jane.
2) We could change myhaircolor to something other than blond, red or brunette. This would cause the send test to fail ( @me->AssertStatusOKViaMacro("",newperson->%Save(),"Person Created OK") ) because we can only set hair colors to values allowed (see the Hair property in MyDemo.Person class)
Because we have already setup the ^UnitTestRoot global we only need to perform 3) and 4) from the above steps.
Unit testing is useful. Edge cases must be determined and tested to make sure the code returns the results we expect. Determining what code to develop a unit test for and what code not to can be difficult. But overtime having a collection of unit tests that verify you code can save time by verifying the code you have developed is valid simply by unit testing.
We have barely scratched the surface with these examples. These two examples are a good starting point. Review the %Unit.TestCase documentation (
http://docs.intersystems.com/cache20121/csp/documatic/%25CSP.Documatic.cls?APP=1&LIBRARY=%25SYS&CLASSNAME=%25UnitTest.TestCase ) and go through the tutorial on Unit Testing (
http://docs.intersystems.com/cache20121/csp/docbook/DocBook.UI.Page.cls?KEY=TUNT_Part2 )