Brief introduction to Test Driven Development with Caché and CosFaker
Henry "HammZ" Hamon
Posted on February 14, 2021
Hello everyone,
I was first introduced to TDD almost 9 year ago, and I immediately fell in love with it.
Nowadays it's become very popular but, unfortunately, I see that many companies don't use it. Moreover, many developers don't even know what it is exactly or how to use it, mainly beginners.
Overview
My goal with this article is to show how to use TDD with %UnitTest. I will show my workflow and explain how to use cosFaker, one of my first projects, which I created using Caché and recently uploaded to OpenExchange.
So buckle up and let's go.
What is TDD?
Test Driven Development (TDD) can be defined as a programming practice that instructs developers to write new code only if an automated test has failed.
There are tons of articles, lectures, talks, whatever, about its advantages and all of them are correct.
Your code is born already tested, you ensure that your system actually meets the requirements defined for it avoiding over-engineering, and you have constant feedback.
So why not use TDD? What is the problem with TDD? The answer is simple: the Cost! It costs a lot!
Because you have to write more lines of code with TDD, it is a slow process. But with TDD you have a final cost of creating a product NOW, without having to add to it in the future.
If you run the tests all the time you find errors early, thus reducing the cost of their correction.
So my advice: Just Do it!
Setup
InterSystems has a documentation and tutorial on how to use %UnitTest, you can read it here.
I use vscode to develop. This way I create a separate folder for tests. I add my project code path to UnitTestRoot and when I execute tests I pass the name of the test subfolder. And I always pass the qualifier loadudl
Set ^UnitTestRoot = "~/code"
Do ##class(%UnitTest.Manager).RunTest("myPack","/loadudl")
Steps
Probably you have heard about the famous TDD cycle: red ➡ green ➡ refactor. You write a test that fails, you write a simple production code to make it pass and you refactor the production code.
So let's get the hands dirty and create a class to do some maths and another class to test it. The latter class shall extend %UnitTest.TestCase.
Now let's create a ClassMethod to return a square of an integer number:
Class Production.Math
{
ClassMethod Square(pValue As %Integer) As %Integer
{
}
}
And test what will happen if we pass 2. It must return 4.
Class TDD.Math Extends %UnitTest.TestCase
{
Method TestSquare()
{
Do $$$AssertEquals(##class(Production.Math).Square(2), 4)
}
}
If you run:
Do ##class(%UnitTest.Manager).RunTest("TDD","/loadudl")
Red! The next step is to make it Green.
To make it work let´s return 4 as a result of execution of our Square method.
Class Production.Math
{
ClassMethod Square(pValue As %Integer) As %Integer
{
Quit 4
}
}
Probably you aren't very happy with this solution, because it actually works for only one scenario. Fine! Let's take the next step. Let's create another test scenario, now sending a negative number.
Class TDD.Math Extends %UnitTest.TestCase
{
Method TestSquare()
{
Do $$$AssertEquals(##class(Production.Math).Square(2), 4)
}
Method TestSquareNegativeNumber()
{
Do $$$AssertEquals(##class(Production.Math).Square(-3), 9)
}
}
it will Fail again, so let's refactor the production code:
Class Production.Math
{
ClassMethod Square(pValue As %Integer) As %Integer
{
Quit pValue * pValue
}
}
Now everything works fine... This is the cycle of TDD, in small steps.
You must be asking yourself, why do I have to follow these steps? Why do I have to see the test fail?
I've worked in teams that wrote the production code and only later wrote the tests. But I prefer to follow these baby steps for the following reasons:
Uncle Bob (Robert C. Martin) said that writing tests after writing the code is not TDD and instead is called “a waste of time”.
Another detail, when I see the test fail, and later I see it pass, I'm testing the test.
Your test is still a code; and it may contain mistakes too. And the way to test it is to guarantee that it fails and passes when it needs to fail and pass. This way you've "tested the test".
cosFaker
To write good tests you might need to generate test data first. One way to do it is to generate a dump of data and use it in your tests.
Another way is to use cosFaker to easily generate fake data when you need it. https://openexchange.intersystems.com/package/CosFaker
You just need to download the xml file, after that go to Management Portal -> System Explorer -> Classes -> Import. Select the xml file to import, or drag the file in Studio.
Also you can import it using Terminal
Do $system.OBJ.Load("yourpath/cosFaker.vX.X.X.xml","ck")
Localization
cosFaker will add locales files in default CSP application folder. For now there are only two languages: English and Brazilian Portuguese (my native language).
The language of data is chosen according to configuration of your Caché.
The localization of cosFaker is an ongoing process, if you want to help, please don't hesitate to create a localized provider for your own locale and submit a Pull Request.
With cosFaker you can generate random Words, Paragraphs, Phone numbers, Names, Addresses, Emails, Prices, Product Names, Dates, Hexadecimal color codes... etc.
All methods are grouped by subject in classes, i.e. to generate a Latitude you call the method Latitude in class Address
Write ##class(cosFaker.Address).Latitude()
-37.6806
You can also generate Json for your tests
Write ##class(cosFaker.JSON).GetDataJSONFromJSON("{ip:'ipv4',created_at:'date.backward 40',login:'username', text: 'words 3'}")
{
"created_at":"2019-03-08",
"ip":"95.226.124.187",
"login":"john46",
"text":"temporibus fugit deserunt"
}
Here is a full list of cosFaker classes and methods:
- cosFaker.Address
- StreetSuffix
- StreetPrefix
- PostCode
- StreetName
- Latitude
- Output: -54.7274
- Longitude
- Output: -43.9504
- Capital( Location = “” )
- State( FullName = 0 )
- City( State = “” )
- Country( Abrev = 0 )
- SecondaryAddress
- BuildingNumber
- cosFaker.App
- FunctionName( Group= “”, Separator = “” )
- AppAction( Group= “” )
- AppType
- cosFaker.Coffee
- BlendName
- Output: Cascara Cake
- Variety
- Output: Mundo Novo
- Notes
- Output: crisp, slick, nutella, potato defect!, red apple
- Origin
- Output: Rulindo, Rwanda
- BlendName
- cosFaker.Color
- Hexadecimal
- Output: #A50BD7
- RGB
- Output: 189,180,195
- Name
- Hexadecimal
- cosFaker.Commerce
- ProductName
- Product
- PromotionCode
- Color
- Department
- Price( Min = 0, Max = 1000, Dec = 2, Symbol = “” )
- Output: 556.88
- CNPJ( Pretty = 1 )
- CNPJ is the Brazilian National Registry of Legal Entities
- Output: 44.383.315/0001-30
- cosFaker.Company
- Name
- Profession
- Industry
- cosFaker.Dates
- Forward( Days = 365, Format = 3 )
- Backward( Days = 365, Format = 3 )
- cosFaker.DragonBall
- Character
- Output: Gogeta
- Character
- cosFaker.File
- Extension
- Output: txt
- MimeType
- Output: application/font-woff
- Filename( Dir = “”, Name = “”, Ext = “”, DirectorySeparator = “/” )
- Output: repellat.architecto.aut/aliquid.gif
- Extension
- cosFaker.Finance
- Amount( Min = 0, Max = 10000, Dec = 2, Separator= “,”, Symbol = “” )
- Output: 3949,18
- CreditCard( Type = “” )
- Output: 3476-581511-6349
- BitcoinAddress( Min = 24, Max = 34 )
- Output: 1WoR6fYvsE8gNXkBkeXvNqGECPUZ
- Amount( Min = 0, Max = 10000, Dec = 2, Separator= “,”, Symbol = “” )
- cosFaker.Game
- MortalKombat
- Output: Raiden
- StreetFighter
- Output: Akuma
- Card( Abrev = 0 )
- Output: 5 of Diamonds
- MortalKombat
- cosFaker.Internet
- UserName( FirstName = “”, LastName = “” )
- Email( FirstName = “”, LastName = “”, Provider = “” )
- Protocol
- Output: http
- DomainWord
- DomainName
- Url
- Avatar( Size = “” )
- Slug( Words = “”, Glue = “” )
- IPV4
- Output: 226.7.213.228
- IPV6
- Output: 0532:0b70:35f6:00fd:041f:5655:74c8:83fe
- MAC
- Output: 73:B0:82:D0:BC:70
- cosFaker.JSON
- GetDataOBJFromJSON( Json = “” // JSON template string to create data )
- Parameter Example: "{dates:'5 date'}"
- Output: {"dates":["2019-02-19","2019-12-21","2018-07-02","2017-05-25","2016-08-14"]}
- GetDataOBJFromJSON( Json = “” // JSON template string to create data )
- cosFaker.Job
- Title
- Field
- Skills
- cosFaker.Lorem
- Word
- Words( Num = “” )
- Sentence( WordCount = “”, Min = 3, Max = 10 )
- Output: Sapiente et accusamus reiciendis iure qui est.
- Sentences( SentenceCount = “”, Separator = “” )
- Paragraph( SentenceCount = “” )
- Paragraphs( ParagraphCount = “”, Separator = “” )
- Lines( LineCount = “” )
- Text( Times = 1 )
- Hipster( ParagraphCount = “”, Separator = “” )
- cosFaker.Name
- FirstName( Gender = “” )
- LastName
- FullName( Gender = “” )
- Suffix
- cosFaker.Person
- cpf( Pretty = 1 )
- CPF is the Brazilian Social Security Number
- Output: 469.655.208-09
- cpf( Pretty = 1 )
- cosFaker.Phone
- PhoneNumber( Area = 1 )
- Output: (36) 9560-9757
- CellPhone( Area = 1 )
- Output: (77) 94497-9538
- AreaCode
- Output: 17
- PhoneNumber( Area = 1 )
- cosFaker.Pokemon
- Pokemon( EvolvesFrom = “” )
- Output: Kingdra
- Pokemon( EvolvesFrom = “” )
- cosFaker.StarWars
- Characters
- Output: Darth Vader
- Droids
- Output: C-3PO
- Planets
- Output: Takodana
- Quotes
- Output: Only at the end do you realize the power of the Dark Side.
- Species
- Output: Hutt
- Vehicles
- Output: ATT Battle Tank
- WookieWords
- Output: nng
- WookieSentence( SentenceCount = “” )
- Output: ruh ga ru hnn-rowr mumwa ru ru mumwa.
- Characters
- cosFaker.UFC
- Category
- Output: Middleweight
- Fighter( Category = “”, Country = “”, WithISOCountry = 0 )
- Output: Dmitry Poberezhets
- Featherweight( Country = “” )
- Output: Yair Rodriguez
- Middleweight( Country = “” )
- Output: Elias Theodorou
- Welterweight( Country = “” )
- Output: Charlie Ward
- Lightweight( Country = “” )
- Output: Tae Hyun Bang
- Bantamweight( Country = “” )
- Output: Alejandro Pérez
- Flyweight( Country = “” )
- Output: Ben Nguyen
- Heavyweight( Country = “” )
- Output: Francis Ngannou
- LightHeavyweight( Country = “” )
- Output: Paul Craig
- Nickname( Fighter = “” )
- Output: Abacus
- Category
Let's create a class for user with a method that returns his username, that will be FirstName concatenated with LastName.
Class Production.User Extends %RegisteredObject
{
Property FirstName As %String;
Property LastName As %String;
Method Username() As %String
{
}
}
Class TDD.User Extends %UnitTest.TestCase
{
Method TestUsername()
{
Set firstName = ##class(cosFaker.Name).FirstName(),
lastName = ##class(cosFaker.Name).LastName(),
user = ##class(Production.User).%New(),
user.FirstName = firstName,
user.LastName = lastName
Do $$$AssertEquals(user.Username(), firstName _ "." _ lastName)
}
}
Refactoring:
Class Production.User Extends %RegisteredObject
{
Property FirstName As %String;
Property LastName As %String;
Method Username() As %String
{
Quit ..FirstName _ "." _ ..LastName
}
}
Now we are going to add an account expiry date and validate it.
Class Production.User Extends %RegisteredObject
{
Property FirstName As %String;
Property LastName As %String;
Property AccountExpires As %Date;
Method Username() As %String
{
Quit ..FirstName _ "." _ ..LastName
}
Method Expired() As %Boolean
{
}
}
Class TDD.User Extends %UnitTest.TestCase
{
Method TestUsername()
{
Set firstName = ##class(cosFaker.Name).FirstName(),
lastName = ##class(cosFaker.Name).LastName(),
user = ##class(Production.User).%New(),
user.FirstName = firstName,
user.LastName = lastName
Do $$$AssertEquals(user.Username(), firstName _ "." _ lastName)
}
Method TestWhenIsNotExpired() As %Status
{
Set user = ##class(Production.User).%New(),
user.AccountExpires = ##class(cosFaker.Dates).Forward(40)
Do $$$AssertNotTrue(user.Expired())
}
}
Refactoring:
Method Expired() As %Boolean
{
Quit ($system.SQL.DATEDIFF("dd", ..AccountExpires, +$Horolog) > 0)
}
Now let’s test when account is expired:
Method TestWhenIsExpired() As %Status
{
Set user = ##class(Production.User).%New(),
user.AccountExpires = ##class(cosFaker.Dates).Backward(40)
Do $$$AssertTrue(user.Expired())
}
And Everything is green..
I know these are silly examples, but this way you will have simplicity not only in the code but also in the class design.
Conclusion
In this article you have learnt a little bit about Test Driven Development and how to use the %UnitTest class.
We also covered cosFaker and how to generate fake data for your tests.
There's much more to learn about tests and TDD, how to use these practices with legacy code, integration tests, acceptance tests (ATDD), bdd, etc...
If you want to know more about it, I strongly recommend 2 books:
Test Driven Development Teste e design no mundo real com Ruby - Mauricio Aniche, I really don't know if this book has english version. There are editions for Java, C#, Ruby and PHP. This book blew my mind with its awesomeness.
And of course, Kent Beck's book Test Driven Development by Example
Feel free to leave any comments or questions.
That's all folks
Posted on February 14, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
February 14, 2021