.NET Tools
Essential productivity kit for .NET and game developers
Advanced course in dotMemory Unit
Some of you know one of our most recently released .NET products: dotMemory Unit, a unit testing framework that helps check your .NET code for memory issues.
In a nutshell, dotMemory Unit adds memory profiling to your unit testing framework. With dotMemory Unit, you can write tests that check your code for all kinds of memory issues, protect already fixed leaks from regression, and use the TDD approach when writing memory-consuming algorithms.
How is all this implemented? In this post, we would like to shed some light on the internals of dotMemory Unit.
If you need an overview of dotMemory Unit before diving in, start with this blog post that explains the basics, then take a look at some usage examples, or refer to online help.
Test methods
It’s all about tests. First of all, dotMemory Unit tries to detect which of your methods are tests. Why does it need that?
First, because you might want to decorate your test with a special attribute to tell dotMemory Unit what to do. For example, you can ask it to check the amount of memory traffic in a test with [AssertTraffic(AllocatedSizeInBytes = 300000)]
.
The second reason is that it needs to know exactly when each test starts and ends in order to save a memory snapshot if a test fails on a “memory” assertion.
For most unit testing frameworks, namely MSTest, NUnit, xUnit.net, MbUnit and csUnit, dotMemory Unit detects test methods automatically by injecting its calls into the beginning and the end of all tests.
Even if your unit testing framework of choice is not on the list of frameworks supported out of the box, you still can use dotMemory Unit. You can find more details on how to do this later in this post.
Why use lambdas in dotMemory.Check()
In order to get memory state and make assertions you should write something like this in your test:
dotMemory.Check(memory => Assert.That( memory.GetObjects( where => where.Type.Is()) .ObjectsCount, Is.EqualTo(0)));
Why do we need a lambda as an argument of the dotMemory.Check()
method to do such a simple thing? There are several reasons for this.
First, you don’t need to write a special “memory test” to detect memory issues, it is enough to add an assertion on memory state to your regular unit test.[1] Such a test (checking both memory and logic) can be run under dotMemory Unit or as an ordinary test. In the latter case, the test will fail as there’s no valid memory data. And this is exactly why we need dotMemory.Check()
: the lambda passed into this method will not be called at all if tests are running without dotMemory Unit.[2]
The second reason of having a lambda as an argument in dotMemory.Check()
is to know whether a test failed on checking memory state or on checking business logic: it is useful to have snapshots from tests failed on checking memory to go straight to memory problems.
-
There is an example of such tests (see
- By default, if
dotMemory.Check()
is called without dotMemory Unit, it will fail. If you need the test to pass by ignoringdotMemory.Check()
, mark the method, class or assembly with the attribute[DotMemoryUnit(FailIfRunWithoutSupport = false)]
RemovePetriDish
test). It is a normal unit test, checking that there are no items in a collection after removing the last item, but it also checks that there are no items retained in memory, which means no memory leaks.
Using dotMemoryApi instead of dotMemory.Check()
dotMemory.Check()
is suitable for most common simple cases but if you don’t like its syntax or find it unsuitable for any other reason, rest assured there’s an alternative available.
In the following example, dotMemory.Check()
looks out of place when memory usage data is gathered during the test and checked just once in the end:
int beforeState = 0; int afterState = 0; //... dotMemory.Check(memory => beforeState = memory.GetObjects(where => where.Type.Is()).ObjectsCount); // ... dotMemory.Check(memory => afterState = memory.GetObjects(where => where.Type.Is ()).ObjectsCount); // and in the end of the test Assert.AreEqual(beforeState, afterState);
What is the other option? At the heart of dotMemory Unit, there’s the dotMemoryApi
class, which is quite simple:
class dotMemoryApi { bool IsEnabled{get; set;} Snapshot GetSnapshot(); SnapshotDifference GetDifference(Snapshot snapshot1, Snapshot snapshot2); Traffic GetTrafficBetween(Snapshot snapshot1, Snapshot snapshot2); void SaveCollectedData(string directoryPath); bool CollectAllocations{get; set;} }
The above example can be rewritten using dotMemoryApi
as follows:
var snapshot1 = dotMemoryApi.GetSnapshot(); // ... var snapshot2 = dotMemoryApi.GetSnapshot(); // and in the end of the test if(dotMemoryApi.IsEnabled) { var beforeState = snapshot1.GetObjects(where => where.Type.Is()).ObjectsCount; var afterState = snapshot2.GetObjects(where => where.Type.Is ()).ObjectsCount; Assert.AreEqual(beforeState, afterState); }
But how do we tell dotMemory Unit to save snapshots if a test failed on a memory assertion? Of course, you can use the “save on any fail” auto-saving strategy, but it is not always convenient. Let’s write a single method that uses a lambda as well but looks not as awkward as in the initial example…
class dotMemoryUnit { public static void AssertBlock(Action action) { if(!dotMemoryApi.IsEnabled) return; try { action(); } catch (Exception) { DotMemoryUnitController.TestFailed(); } } }
…and rewrite the assertion block from our last example:
dotMemoryUnit.AssertBlock(() => { var beforeState = snapshot1.GetObjects(where => where.Type.Is()).ObjectsCount; var afterState = snapshot2.GetObjects(where => where.Type.Is ()).ObjectsCount; Assert.AreEqual(beforeState, afterState); });
This call will save a workspace with memory snapshots in case of a failure. As an added bonus, it just looks neat, doesn’t it?
Let’s take a closer look at DotMemoryUnitController
and see how it can be useful when dealing with testing frameworks that aren’t supported out of the box.
Unsupported testing frameworks
What should you do if your favorite unit testing framework is not supported by dotMemory Unit out of the box? The answer is, adapt it manually! In fact, all you need is to tell dotMemory Unit where your test method starts, ends and fails. All this can be done using a single DotMemoryUnitController
class:
public static class DotMemoryUnitController { public static void TestStart(MethodBase testMethod = null); public static void TestFailed(bool onMemoryAssert = true); public static void TestEnd(); }
To make it easier and more elegant, you can write your own wrapper or use this one. At the end, your test will look like this (even attributes will work fine):
[AssertTraffic(AllocatedObjectsCount = 100500)] public void Test() { using (dotMemoryUnit.Support()) { // any dotMemory Unit calls you want here } }
Although DotMemoryUnitController
is mostly designed for unsupported testing frameworks, it also can be used in combination with dotMemoryApi
to achieve behavior similar to the dotMemoryUnit.AssertBlock()
example above.
More ways to tune
There are many ways how you can fine-tune dotMemory Unit to your liking. For example, if you don’t like the lambda in the GetObjects()
method, you can write something like this:
public static class Type { public static Query Is() { return TypeProperty.Instance.Is (); } } public static class Extension { public static ObjectSet GetObjects(this Snapshot snapshot, Query query) { return snapshot.GetObjects(_ => query); } }
and then completely avoid lambdas in your tests:
dotMemoryApi .GetSnapshot() .GetObjects(Type.Is()) .ObjectsCount;
Also, don’t miss QueryBuilder. This powerful tool allows creating reusable queries to make your tests more maintainable. For example, we can rewrite one of the examples from this article using QueryBuilder
to avoid code duplication:
var snapshot1 = dotMemoryApi.GetSnapshot(); // ... var snapshot2 = dotMemoryApi.GetSnapshot(); // and in the end of the test dotMemoryUnit.AssertBlock(() => { // reuse the query whenever you need var typeIsFoo = QueryBuilder.GetObjects(where => where.Type.Is()); var beforeState = snapshot1.GetObjects(typeIsFoo).ObjectsCount; var afterState = snapshot2.GetObjects(typeIsFoo).ObjectsCount; Assert.AreEqual(beforeState, afterState); });
If you’re using core classes dotMemoryApi
, DotMemoryUnitController
and QueryBuilder
and have a general idea of how dotMemory Unit works inside, you can customize almost everything and write your perfect framework.
Whether you want to go that far or you use the default framework, dotMemory Unit will help you replace time-consuming manual profiling with automated memory tests.