As a developer the most important thing I produce is clean, functional, and performant code. Of course, I must write code that will be used in applications, but more importantly I must test the code under any conditions I expect it to encounter as well as conditions I haven't foreseen. In order to do this, I leverage C3 Integrated Development Studio (C3 IDS) for continuous integration along with industry-standard testing frameworks like Jasmine, Mocha, and RSpec.
For my work at C3.ai, I use Jasmine for unit-testing the functions in my UI components and the backend API logic. I also use an internal C3.ai tool called Skywalker that leverages Selenium for integration-testing the application logic in the UI. By testing my code as close to a development environment as possible, I have more confidence the applications I am working on will continue to run under all conditions. Testing doesn't mean everything will be perfect and there are still gaps that developers must be aware of, but I’d like to focus in this blog on the benefits of testing using Jasmine (along with the Enzyme testing framework) and some of the best practices we use at C3.ai.
Readable Tests (descriptions)
How should developers describe their tests? A great test should be readable both in code and in the test descriptions. The test results produced by Jasmine concatenate the descriptions of blocks until the final test block is reached. In the example below, the two tests results are:
- test_Greeting render should render a title
- test_Greeting addSalutation should append Hey before user.name
* Outer most describe telling us what file is tested
* Additional describe separating what logic is being tested
* Final description describing the expected behavior
The color code above explains what is happening at each level.
Readable Tests (writing)
How should developers structure their tests? To help write readable test code, Jasmine offers beforeAll and beforeEach along with their counterparts afterAll and afterEach. These blocks help to set up context that will be shared across different subsets of tests. As tests get more granular, additional describe blocks can be added. A good rule of thumb is every branch (different path of an if block or switch statement) should have a describe block and the final statement should have an it block.
In the example below, the developer has pulled out a function that will create a Jasmine spy and return a specific value to help test the advancedSalutation function. Another important part of test writing is making sure the description matches the execution of the test. An approach I take when writing my test description and the implementation is to think about what happens if this test starts failing and I am not available to explain my code. The code and the test description together should explain what is going on and being tested. If this is not the case, I go back and address the gap. If the description and the test implementation are conflicting, it is impossible to tell why the test is failing without getting deep into the code, so keeping these two unified is crucially important.
The Value of Testing
Why should developers care about writing and maintaining tests? In my development experience, one of the worst feelings is finding out the code you built is no longer working. In order to minimize this occurrence, I have found a new appreciation for writing and maintaining tests. I am more confident in the features I deliver when I know I have written tests that will cover all expected cases and handle all edge cases without breaking. Because C3.ai uses a continuous integration pipeline, if another developer breaks a feature I wrote, the error will surface BEFORE it gets into the code base.
By writing tests that handle all the ways your feature will be used, you can rest assured that any changes to these APIs or implementations will surface errors in your code components and will (hopefully) be addressed by the developer responsible for making changes that break the code. Tests also give developers confidence to refactor code down the road when more features are needed. Keeping a component working before and after a refactor gives me (the developer) confidence that the refactor was successful and end users will not experience any loss of functionality.
The value that testing provides developers, organizations, and end users is worth the effort. Well-tested code gives confidence to an organization that the products they build and deliver to their customers will be functional, elegant, and provide value. For developers, we know our code will run under different conditions when it is well tested. As an organization, we know all the moving parts of our code base will function together cohesively and ensure delivery of a great product. As an end user, I trust the products I am using and will continue to trust the organization providing the tools I use in my everyday life.