Can unit tests and memory profiling be combined?
- Transfer
If unit testing is an integral part of your development process, then you regularly run numerous tests to verify the functionality of the application. Now imagine that you can write some special “memory tests”. For example, a test that detects a leak by checking the memory for the presence of objects of a certain type, or a test that monitors memory traffic and “drops” if the traffic (allocated volume) exceeds a specified threshold. This is exactly what the dotMemory Unit framework allows you to do . dotMemory Unit is distributed as a NuGet package and allows you to run the following scripts:
- Checking the memory for the presence of objects of a certain type.
- Checking memory traffic.
- Comparison of snapshots (hereinafter referred to as snapshots) of memory.
- Saving snapshots to disk for later analysis in dotMemory (JetBrains memory profiler).
In other words, dotMemory Unit extends the capabilities of your unit testing framework with the functionality of a memory profiler.
How it works?
- dotMemory Unit is distributed as a NuGet package installed in your test project:
PM> Install-Package JetBrains.DotMemoryUnit - dotMemory Unit requires the runner unit test included with ReSharper. Therefore, to run the dotMemory Unit tests, ReSharper 9.1 or dotCover 3.1 must be installed on your machine.
- After installing the dotMemory Unit package, an additional Run Unit Tests under dotMemory Unit item will appear in the ReSharper menu . In this mode, the runner test will make dotMemory Unit calls along with the rest of the code. If you run this test as usual (without dotMemory Unit support), all calls to the dotMemory Unit framework will be ignored.

- dotMemory Unit is compatible with all unit testing frameworks supported by ReSharper, including MSTest and NUnit.
- A separate 'launcher' for integration with CI systems like JetBrains TeamCity is planned in one of the next releases.
- dotMemory Unit is absolutely free.
Example 1: Checking memory for specific objects
Let's start with something simple. One of the most useful scenarios is to identify a leak by checking the memory for objects of a certain type.
[Test]
public void TestMethod1()
{
... // делаем что-нибудь
// предполагаем, что в памяти осталось 0 объектов типа Foo
dotMemory.Check(memory => //1, 2
{
Assert.That(memory.GetObjects(where => where.Type.Is()).ObjectsCount, Is.EqualTo(0)); //3
});
}
- The lambda is passed to the
Checkstatic class methoddotMemory. This method will only be called if you run this test using the Run Unit Tests under dotMemory Unit menu . - The object
memorypassed to the lambda contains data about all the objects in memory at the current point of program execution. - The method
GetObjectsreturns a set of objects matching the condition passed in the next lambda. For example, a given line of code selects only objects of type from memoryFoo. The expressionAssertassumes that there must be0objects of type in memoryFoo.
Note that dotMemory Unit does not oblige you to use any specific syntax forAssert. Just use the syntax of the framework your test is written for. For example, the line from the example above (written for NUnit) can be rewritten for MSTest:Assert.AreEqual(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount);
dotMemory Unit allows you to select objects by almost any condition, receive data by the number of objects and use them in
Assertexpressions. For example, you can verify that the Large object heap does not contain objects: Assert.That(memory.GetObjects(where => where.Generation.Is(Generation.Loh)).ObjectsCount, Is.EqualTo(0));
Example 2: Checking memory traffic
The test for checking memory traffic (allocated data volume) looks even simpler. All that is required of you is to "mark" the test using the attribute
AssertTraffic. In the following example, we assume that the memory size allocated by the test TestMethod1does not exceed 1000 bytes.[AssertTraffic(AllocatedMemoryAmount = 1000)]
[Test]
public void TestMethod1()
{
... // какой-то код
}
Example 3: Complex scripts for checking memory traffic
If you need more detailed information about traffic (for example, data on allocations of objects of a certain type), you can use an approach similar to that shown in Example 1. Lambdas passed to the method
dotMemory.Checkallow you to filter data according to various conditions.var memoryCheckPoint1 = dotMemory.Check(); // 1
foo.Bar();
var memoryCheckPoint2 = dotMemory.Check(memory =>
{
// 2
Assert.That(memory.GetTrafficFrom(memoryCheckPoint1).Where(obj => obj.Interface.Is()).AllocatedMemory.SizeInBytes,
Is.LessThan(1000));
});
bar.Foo();
dotMemory.Check(memory =>
{
// 3
Assert.That(memory.GetTrafficFrom(memoryCheckPoint2).Where(obj => obj.Type.Is()).AllocatedMemory.ObjectsCount,
Is.LessThan(10));
});
- In order to mark the time period at which you want to analyze traffic, use the "checkpoints" created by the same method
dotMemory.Check(as you might have guessed, this method simply removes the memory snapshot at the time of the call). - The checkpoint defining the starting point of the interval is passed to the method
GetTrafficFrom.
For example, this line assumes that the total size of objects implementing the interfaceIFooand created betweenmemoryCheckPoint1andmemoryCheckPoint2does not exceed 1000 bytes. - You can receive data on any of the previously created checkpoints. So, this line requests traffic data between the current call
dotMemory.CheckandmemoryCheckPoint2.
Example 4: Comparing Snapshots
As in the "adult" dotMemory profiler, you can use checkpoints not only to analyze traffic, but also to compare them with each other. In the example below, we assume that none of the objects belonging to the namespace
MyAppsurvived the garbage collection in the interval between memoryCheckPoint1and the second call dotMemory.Check. var memoryCheckPoint1 = dotMemory.Check();
foo.Bar();
dotMemory.Check(memory =>
{
Assert.That(memory.GetDifference(memoryCheckPoint1)
.GetSurvivedObjects().GetObjects(where => where.Namespace.Like("MyApp")).ObjectsCount, Is.EqualTo(0));
});
Conclusion
dotMemory Unit is very flexible and allows you to fully control the memory usage of your application. Use “tests for memory” as you would use regular tests:
- After you yourself discover a memory leak, write a test that covers this part of the code.
- Write integration tests using the dotMemory Unit to make sure that the new features do not create memory problems.