Date: Thu, 28 Mar 2024 15:33:17 +0000 (UTC) Message-ID: <879618240.51.1711639997131@bdeb7b650a5f> Subject: Exported From Confluence MIME-Version: 1.0 Content-Type: multipart/related; boundary="----=_Part_50_1872247860.1711639997131" ------=_Part_50_1872247860.1711639997131 Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: quoted-printable Content-Location: file:///C:/exported.html
Writing unit tests is an essential part of development in RAMClo= ud. We don't consider a coding project "done" until it has good unit tests,= and in particular you shouldn't push changes to the main repository unless= they have proper unit tests. Our goal is to get (nearly) 100% test c= overage by lines of code. Here are some of the conventions we follow when w= riting unit tests:
This is the most important rule. The structure of the tests should match= the structure of the code:
Making the tests isomorphic to the code makes it much easier to maintain= the tests as the code evolves: when you modify the code, it's easy to find= any existing tests for that section of code, so you can see whether = you need to add additional tests. If you restructure a code file by moving = methods around, you must restructure the test file to match.
We write unit tests to test the specific implementation of a feature, no= t just its interface. Typically, you should be looking at the code wh= en writing tests, and you should write unit tests to exercise each piece of= the code. This means that, in general, you can't write the tests before yo= u've written the code (but, see the following rule for an exception).
Whenever you fix a bug, there should be a test for that bug fix. Further= more, you should write the test before fixing the bug: write the t= est, make sure it triggers the bug, then fix the bug and make sure that the= test now passes. If you wait until after fixing the bug before writing the= test, it's easy to end up with a test that doesn't actually exercise the b= ug. Or, alternatively, you can write the test after the bug has been fixed,= but then you should back out the bug fix temporarily to make sure that the= test really fails.
It's okay to have a few unit tests that exercise overall functionality, = but most tests should be very focused. Each test should validate a specific= condition or section of code. It's better to have many individual small te= sts, rather than one large test that validates many different conditions an= d/or code sections. Tests that do long sequences of operations are hard to = understand and maintain, so split them up.
Sometimes there's a fair amount of setup that must be done before you ca= n run the actual test, and the setup is the same for every test. This might= lead you to believe you should simply combine all of the tests together in= to one mega-test see you don't have to repeat the setup code over and over,= but that's not the best way to handle this. Instead, write a method in the= test class that does the setup, then just invoke this one method from each= of a collection of smaller tests. Or, do the setup in the constructor for = the test class.
Ideally, each class should be tested in isolation. However, some higher-= level classes have complex dependencies on lower-level classes that can mak= e unit testing hard (e.g. we don't want to have to set up an running RAMClo= ud cluster to run unit tests, but some classes will invoke RPCs to other se= rvers). In situations like this, write mock classes that can be used instea= d of lower-level classes to simplify writing unit tests. We have alre= ady written many such classes. For example, if a class invokes system calls= , there is a MockSyscall class that can be used to avoid actually making sy= stem calls.
It's easy to write tests that are extremely sensitive to the overall str= ucture of the system, such that the tests will break if almost any co= de changes. When this happens, it becomes hard to make code changes, becaus= e we have to update a lot of tests afterwards. Try to avoid such tests. Ide= ally, a test is sensitive to the behavior of a particular piece of code (th= e code it is validating), but not sensitive to other code in the system. On= e way to create more robust tests is to setup a test by invoking API method= s of a class, which are less likely to change, rather than by directly modi= fying fields of structures, which are more likely to change.
Please avoid tests that take a long time to run (e.g. hundreds of millis= econds or longer). Our entire unit test suite currently runs in about 10-20= seconds, and we'd like to keep the running time from growing much beyond t= his, so that it's not painful to run the tests. Occasionally long-running t= ests are unavoidable, but to avoid them as much as possible.
When testing asynchronous behaviors of the system, it's tempting to writ= e tests that look like this:
initiate some asynchronous action; sleep for a while; check that the action produced the expected results;
Unfortunately, the timing for the asynchronous action may not very= predictable, which creates a dilemma. If you set the sleep time low, then = the test may fail because of variations in running speed (e.g., due to a hi= gh load on the machine). If you set the sleep time high enough to be = absolutely certain that the action can complete, then the test will t= ake a long time to run.
It's better to structure such tests like this:
initiate some asynchronous action; enter a loop, waiting for a short interval and then checking for the expect= ed results; after many iterations of the loop, fail the test;
This has the advantage that the test will almost always complete quickly= . You can be conservative in allowing the loop to execute many iterations b= efore failing, since this will almost never happen.
Although in general we expect all code to have unit tests, there a= re a few situations where it isn't really possible to write unit tests. One= example is our low-level network drivers. These classes tend to be highly = dependent on the NIC hardware; it typically isn't possible to run tests usi= ng actual NICs, and if the NIC is mocked out then the test isn't particular= ly useful (the code might work with the mocked NIC but not with the real on= e). In addition, drivers tend to have fairly straight-line code, so almost = all of it gets exercised immediately in a real RAMCloud cluster. Don't give= up easily on writing unit tests, though; in most cases iit's possible to f= ind a way to write meaningful tests.