Closed pjcozzi closed 8 years ago
This is ready. Please provide tech comments by end of Wednesday so this can go through copyediting. If you want to make a small change or add a section, just do it.
Last call for tech feedback.
This is a good write up. My one overall comment is that it's wierd that you are using the first person singular voice (unless this is meant to be a blog post first?). If it is going to eventually live on the wiki, then it is ultimately a commuity document that will be updated and edited by everyone and it's odd to have personal anecdotes in it.
Other feedback:
defineSuite
is also a custom Cesium function that wraps Jasmine define calls and provides the category capability that's not part of core Jasmine. (categories are also not part of Jasmine). All of this functionality is part of Specs/spec-main.js
it
function with our own version in spec-main.js
.toBeRejected
helper.Though after looking at this we should add a toBeRejected helper.
I'll add the example in the meantime, but perhaps submit an issue for toBeRejected
.
Thanks for the feedback. Updated. This will be part of the contributor guides, not a blog post. I put the first person paragraphs in block quotes.
Anyone else have feedback?
TODO
Testing Guide
Our development culture has a commitment to testing. Cesium is used in diverse use cases on a wide array of platforms so it is important for it to be well tested.
As of Cesium 1.15, Cesium has over 7,000 tests with 93% code coverage. Cesium has almost as much test code (94K lines) as engine code (98K). We are unaware of any other project of this size and lifetime and with this many contributors that has similar stats.
All new code should have 100% code coverage and should pass all tests. Always run the tests before opening a pull request.
Running the Tests
The Cesium tests are written in JavaScript and use Jasmine, a behavior-driven testing framework. Jasmine calls an individual test, e.g., a function with one or more assertions, a spec (however, the Cesium team usually still say "test"), and a group of related tests, e.g., all the tests for
Cartesian3
, a suite. Jasmine also calls an assertion, an expectation.When running Cesium locally, browse to http://localhost:8080/ and there are several test options:
Run all tests (Run with WebGL validation)
Runs all the tests. As of Cesium 1.15, on a decent laptop, they run in about a minute in Chrome. It is important that the tests run quickly so we run them often.
When all the tests pass, the page looks like:
When one or more tests fail, the page looks like:
In this case, the number of failing tests is listed at the top, and details on each failure is listed below, including the expected and actual value of the failed expectation and the call stack. The top several functions of the call stack are inside Jasmine and can be ignored. Above, the file and line of interest for the first failing test starts with an
@
:Click on the failed test to rerun just that test. This is useful to save time when fixing an issue by avoiding rerunning all the tests. Always rerun all the tests before opening a pull request.
The link to Run with WebGL validation passes a query parameter to the tests to enable extra low-level WebGL validation such as calling
gl.getError()
after each WebGL call. We use this when doing the monthly Cesium release and when making changes to Cesium's renderer.Select a test to run
This option loads the test page without running any tests.
We can then use the browsers' built-in search to find a test or suite and run only it. For example, below just the tests for
Cartesian3
were run.This uses a query parameter to select the test/suite to run so refreshing the page will run just that test/suite again.
Often when developing, it is useful to run only one suite, to save time, instead of all the tests, and then run all the tests before opening a pull request.
Run only WebGL tests
Suites can have a category associated with them. This option runs all tests in the
WebGL
category, which includes all tests that use WebGL (basically anything that requires creating aViewer
,CesiumWidget
,Scene
, orContext
).Run only non-WebGL tests
Likewise, this option runs all tests not in the WebGL category.
Perhaps surprising, this is the bulk of Cesium tests, which include math and geometry tests, imagery provider tests, data source tests, etc.
These tests run quickly (for example, 15 seconds compared to 60) and are very reliable across systems since they do not rely on the underlying WebGL implementation, which can vary based on the browser, OS, driver, and GPU.
Run all tests against combined file (Run all tests against combined file with debug code removed)
Most test options load Cesium using the individual source files in the
Source
directory, which is great for debugging.However, many users build apps using the built Cesium.js in
Build/Cesium
(which is created, for example, by runningnpm run combine
). This option runs the tests using this instead of individual Cesium source files.The Run all tests against combined file with debug code removed is the same except it is for use with the release version of the built Cesium.js (which is created, for example, by running
npm run combineRelease
). The release version hasDeveloperError
exceptions optimized out so this test option makestoThrowDeveloperError
always pass.See the Contributor's Guide for all the Cesium build options.
Run all tests with code coverage (build 'instrumentForCoverage' first.)
JSCoverage is used for code coverage. It is especially important to have outstanding code coverage since JavaScript doesn't have a compiler and linker to catch early errors.
To run code coverage, first create a build of Cesium that is instrumented for coverage by running
npm run instrumentForCoverage
. Currently, this is Windows only.Then use this test option to run the tests with code coverage. Click on the
Summary
tab to see the total code coverage and coverage for each individual source files.Click on a file to see line-by-line coverage for just that file. For example, here is
AssociativeArray
:In the left margin, green indicates a line that was executed and red indicates a line that was not. Many lines, like comments and semicolons, are not colored since they are not executable.
For the
contains
function above:AssociativeArray.prototype.contains = function(key) {
is executed once when Cesium is loaded to assign thecontains
function to theAssociativeArray
's prototype.if
statement and return statement are executed 3,425 times.throw
statement is not executed, which indicates that test coverage should be improved here. We strive to test all error conditions.When writing tests do not confuse 100% code coverage with 100% tested. For example, it is possible to have 100% code coverage without having any expectations. Also consider the following code:
It is possible to have 100% code coverage with two tests: one test where
a
andb
are bothtrue
, and another when both arefalse
; however, this only takes into account the case when// Code block a.1
and// Code block b.1
run together or when// Code block a.2
and// Code block b.2
run. There could be an issue when, for example,// Code block a.1
and// Code block b.2
run together.The number of linearly independent paths (four in this case) is called the cyclomatic complexity. Be mindful of this when writing tests. On one extreme, 100% code coverage is the least amount of testing, on the other extreme is covering the cyclomatic complexity, which quickly becomes unreasonable. Use your knowledge of the implementation to devise the best strategy.
Testing Previous Versions of Cesium
Sometimes it is useful to see if an issue exists in a previous version of Cesium. The tests for all versions of Cesium back to b15 (April 2013) are hosted on the Cesium website via the downloads page. Use the "Documentation, Sandcastle, tests, etc." links.
testfailure
Label for IssuesDespite our best efforts, sometimes tests fail. This is often due to a new browser, OS, or driver bug that breaks a test that previously passed. If this indicates a bug in Cesium, we strive to quickly fix it. Likewise, if it indicates that Cesium needs to workaround the issue (for example, as we did for Safari 9), we also strive to quickly fix it.
If a test failure is likely due to a browser, OS, or driver bug, or a poorly written test, and the failure does not impact actual Cesium apps, we sometimes submit an issue with the testfailure label to fix it at a later time. A great way to contribute to Cesium is to help fix these issues.
Writing Tests
We love to write tests. We often write them as we write engine code (meaning Cesium itself) - or if the engine code is experimental, we make a second pass and write tests before opening a pull request. Sometimes we do both; we write tests right away for the new code we expect to be stable, and we wait to write tests for the code in flux.
Directory Organization
Tests are located in the Specs directory (recall, Jasmine calls a test a "spec"), which has a directory structure that mirrors the Source directory. For example, all the tests for files in
Source/Core
are inSpecs/Core
. Likewise, all the tests forSource/Core/Cartesian3.js
are inSpecs/Core/Cartesian3Spec.js
. The filenames are the same except for theSpec
suffix. Each spec file corresponds to at least one suite (sometimes suites are nested inside).Bottom-Up Unit Testing
The Cesium tests are largely unit tests because they test individual units, e.g., functions or classes. The simplest units are tested individually, and then units built upon other units are also tested. This allows us to build Cesium on well-tested foundations and to quickly narrow down issues.
For example, a
BoundingSphere
is composed of aCartesian3
that defines its center and a number that defines its radius. Even though tests forBoundingSphere
implicitly test parts ofCartesian3
, there are separate tests that explicitly testCartesian3
as a unit so anything that relies onCartesian3
knows it is already tested.Often, we also test private units individually for the same reason. For example,
ShaderCache
is a private class in Cesium used by primitives, but it is still individually tested in ShaderCacheSpec.jsSometimes classes or functions are even designed with a separation specifically to enable more precise testing. For example, see
getStringFromTypedArray
and getStringFromTypedArraySpec.js.Test Code is Code
Tests are written in JavaScript using Jasmine. It is important to realize that the tests themselves are code, just like Cesium. As such, the test code is held to the same standards as the engine code; it should be well organized, cohesive, loosely coupled, fast, and go through peer review.
Testing Basics
Cartesian3Spec.js contains the tests for
Cartesian3
, which is a class representing a 3D point or vector withx
,y
, andz
properties, and typical functions like adding twoCartesian3
objects.Here is a stripped down version of the tests:
defineSuite
identifies this file as a test suite, and include modules just likedefine
is used in engine code. The modules are listed in alphabetical order as usual, except the module being tested is listed first.Using Jasmine, each test is defined by calling
it
and passing a string that describes the test and a function that is the test.This test constructs a default
Cartesian3
object and then expects that thex
,y
, andz
properties are zero (their default) using Jasmine'sexpect
andtoEqual
functions.Tests should have at least one
expect
call, but they may also have several as long as the test is cohesive. A test should test one behavior; if a test grows too complicated, it is hard to debug when it fails. To test one function may only require one test with oneexpect
, or it may require multiple tests, each with multipleexpect
statements. It depends on context. Experience, peer review, and the existing tests will help guide you.The above test does not require creating a
Viewer
widget or even a WebGL context; the only part of Cesium it uses isCartesian3
and anything it depends on.We often can't rely on an exact floating-point comparison. In this case, use
toEqualEpsilon
instead oftoEqual
to compare within a tolerance.toEqualEpsilon
is a custom Jasmine matcher that the Cesium tests add, see Specs/addDefaultMatchers.js for all the custom matchers. In general, all test utility functions are in files in theSpecs
root directory.For more on comparing floating-point numbers, see Comparing Floating Point Numbers, 2012 Edition.
Testing Exceptions
In addition to testing success cases, we also test all failure cases. The custom machters,
toThrowDeveloperError
andtoThrowRuntimeError
, can be used to expect an exception to be thrown.Above,
Cartesian3.fromDegrees
is expected to throw aDeveloperError
because it expects longitude and latitude arguments, and only longitude is provided.Tips:
expect()
in case setup code unintentionally throws an exception.expect
call when first running the test, for example:Before and After Tests and Suites
The Jasmine functions
beforeAll
andafterAll
are used to run a function before and after, respectively, all the tests in a suite. Likewise,beforeEach
andafterEach
run a function before and after each test is ran. For example, here is a common pattern from DebugModelMatrixPrimitiveSpec.js:Above,
scene
is scoped at the suite-level so all tests in the file have access to it. Before the suite is ran,beforeAll
is used to assign toscene
(see below), and after the suite is ran,afterAll
is used to destroy the scene. UsingafterEach
, after each test is ran, all the primitives are removed from the scene.scene
is typically used in a test like this:The test knows
scene
will be defined and does not need to worry about cleaning up thescene
becauseafterEach
andafterAll
take care of it.We strive the write isolated isolated tests so that a test can be run individually and produce the same results as when running the suite containing the test or all Cesium tests. Therefore, a test should not depend, for example, on a previous test setting global state.
The tests in the
'WebGL'
category do not strictly follow pattern. Creating a WebGL context (which is implicit, for example, increateScene
) is slow and creating a lot of contexts, e.g., one per test, is not well supported in browsers. So, the tests use the pattern in the code example below where ascene
(orviewer
orcontext
) has the lifetime of the suite usingbeforeAll
andafterAll
.Rendering Tests
Unlike the
Cartesian3
tests we first saw, many tests need to construct the main CesiumViewer
widget or one of its major components. Low-level renderer tests construct justContext
(which, itself, has a canvas and WebGL context), and primitive tests construct aScene
(which contains aContext
).As shown above, these tests use Cesium test utility functions,
createViewer
,createScene
, orcreateContext
. These functions honor query parameters passed to the tests (for example, like enabling WebGL validation) and add extra test functions to the returned object.For example,
createScene
creates a 1x1 pixel canvas with a Cesium Scene and addsrenderForSpecs
andpickForSpecs
to the returnedScene
object:In the first test,
renderForSpecs
initializes the frame, renders the scene into the 1x1 canvas, and then returns the RGBA value of the rendered pixel. Like most rendering tests, this uses a coarse-grained expectation to check that the pixel is not the default value of black. Although an expectation this coarse-grained may not catch all subtle errors, it is reliable across platforms, and we rarely have bugs a more fine-grained test would have caught, especially with some manual testing (see below).In the second test,
renderForSpecs
is used again, but this time it is to verify that the pixel value is the same as the default background color since the primitive'sshow
property isfalse
.In the final test,
pickForSpecs
executes aScene.pick
for the one-pixel canvas. A typical follow-up expectation verifies the primitive of interest was picked and itsid
is the expected value.GLSL
GLSL is the shading language used by WebGL to run small graphics programs in parallel on the GPU. Under-the-hood, Cesium contains a library of GLSL identifiers and functions. These are unit tested by writing a simple fragment shader that outputs white if the test passes. For example, here is an excerpt from BuiltinFunctionsSpec.js;
createContext
returns aContext
object with a test function,verifyDrawForSpecs
, that renders a point to the 1x1 canvas and verifies the pixel value is white, e.g.,In the test above, the expectation is implicit in the GLSL string for the fragment shader,
fs
, which assigns white togl_FragColor
ifczm_transpose
correctly transposes the matrix.Spies
It can be useful to expect if a function was called and inspect information about the function call like the arguments passed to it. Jasmine spies are used for this.
Here is an excerpt from TweenCollectionSpec.js:
Tweens are used for animation. This test creates a spy with
jasmine.createSpy
to verify that a tween calls the providedcomplete
function when a tween finishes animating usingtoHaveBeenCalled()
, which is immediately in this case givenduration
is0.0
.Spies can also provide more information about the function call (or calls). Here is an excerpt from GeocoderViewModelSpec.js:
Here,
spyOn
is used to replaceCamera.flyTo
(prototype function on instances) with a spy. When the Geocoder is used to search for a location, the test expects thatCamera.flyTo
was called with the right arguments.Spies can also be used on non-prototype functions. Here is an excerpt from ModelSpec.js:
This test verifies that a glTF model uses the expected render state. First, a spy is added to
RenderState.fromCache
. Since we want the spy to collect information but still call the original function,and.callThrough()
is used. Once the model is loaded,toHaveBeenCalledWith
is used to expect thatRenderState.fromCache
was called with the expected arguments.For more examples of what you can do with spies, see the Jasmine examples.
Beware of too tightly coupling a test with an implementation; it makes engine code hard to refactor and results in specific narrow tests. Given that we are usually white box testing (where we know the implementation details, as opposed to black box testing), we need to resist the urge to let too many implementation details leak into a test. In particular, reach into private members (whose names start with
_
) sparingly.Test Data and Services
Sometimes, a test requires sample data, like a CZML file or glTF model, or a service. When possible, we try to procedurally create data or mock a response in the test instead of reading a local file or making an external request. For example, loadArrayBufferSpec.js uses a spy to simulate an XHR response.
When external data can't be avoided, prefer storing a small file in a subdirectory of Specs/Data. Avoid bloating the repo with an unnecessarily large file. Update LICENSE.md if the data requires a license or attribution. Include a README file when useful, for example, see Specs/Data/Models/Box-Textured-Custom.
Make external requests that assume the tests are being used with an Internet connection very sparingly. We anticipate being able to run the tests offline.
Promises
(For an introduction to promises, see JavaScript Promises - There and back again).
For asynchronous testing, Jasmine's
it
function uses adone
callback. For better integration with Cesium's asynchronous patterns, Cesium replace'sit
with a function that can return promises.Here is an excerpt from ModelSpec.js:
Given a model's url,
loadModel
(detailed below) returns a promise that resolves when a model is loaded. Here,beforeAll
is used to ensure that two models, stored in suite-scoped variables,texturedBoxModel
andcesiumAirModel
, are loaded before any tests are ran.An implementation of
loadModel
is:Since loading a model requires asynchronous requests and creating WebGL resources that may be spread over several frames, Cesium's
pollToPromise
is used to return a promise that resolves when the model is ready, which occurs by rendering the scene in an implicit loop (hence the name "poll") untilmodel.ready
istrue
or thetimeout
is reached.pollToPromise
is used in many places where a test needs to wait for an asynchronous event before testing its expectations. Here is an excerpt from BillboardCollectionSpec.js:Here a billboard is loaded using a url to an image. Internally,
Billboard
makes an asynchronous request for the image and then sets itsready
property totrue
. The function passed topollToPromise
just returns the value ofready
, it does not need to render the scene to progressively complete the request likeModel
. Finally, the resolve function (passed tothen
) verifies that the billboard is green.To test if a promises rejects, we call
fail
in the resolve function and put the expectation in the reject function. Here is an excerpt from ArcGisMapServerImageryProviderSpec.js:Mocks
To isolate testing, mock objects can be used to simulate real objects. Here is an excerpt from SceneSpec.js;
This test verifies that
debugCommandFilter
can be used to filter the commands executed when the scene is rendered. Here, the function passed todebugCommandFilter
explicitly filters out the command,c
. In order to ask the scene to execute the command in the first place, a mock object,MockPrimitive
, is used to return the command when the scene is rendered.This test is more cohensive and easier to debug than if it were written using a real primitive, which brings along all of its extra behavior and does not provide direct access to its commands.
Categories
As mentioned above, some tests are in the
'WebGL'
category. To assign a category to a suite, pass the category todefineSuite
.defineSuite
is a custom Cesium function that wraps Jasmine define calls and provides the category capability.Manual Testing
Sometimes running the unit tests is all that is needed to verify new code. However, we often also manually run Cesium to see the effects of new code. Sometimes it is as simple as running Cesium Viewer before opening a pull request perhaps because we just added a new function to
Cartesian3
. Other times, it is as involved as going through each example in Sandcastle and testing the different options because, for example, we refactored the renderer for WebGL 2. Most often, there is a middle ground, for example, we added a new feature toModel
so we run the Sandcastle examples that create 3D Models.Pragmatic Advice
Advice from @pjcozzi:
Start With a Similar (Small) Test
The first 73 Cesium tests from March 2011.
See Section 4.4 of Getting Serious with JavaScript by Cesium contributors Matthew Amato and Kevin Ring in WebGL Insights for a more deep but less broad presentation of Cesium testing.