Back to Home

Can unit tests and memory profiling be combined?

unit testing · profiling

Can unit tests and memory profiling be combined?

Original author: A.Totin
  • Transfer
Memory profilers can hardly be called "utilities for daily use." Most often, developers think about profiling their product before the release. Such an approach may well work, but only until some memory problem discovered at the last moment (for example, a memory leak or large memory traffic) destroys all your plans. One solution might be profiling on a regular basis, but hardly anyone would want to spend such precious time on it. However, the solution seems to be there.

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
    });
}

  1. The lambda is passed to the Checkstatic class method dotMemory. This method will only be called if you run this test using the Run Unit Tests under dotMemory Unit menu .
  2. The object memorypassed to the lambda contains data about all the objects in memory at the current point of program execution.
  3. 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 memory Foo. The expression Assertassumes that there must be 0objects of type in memory Foo.

    Note that dotMemory Unit does not oblige you to use any specific syntax for Assert. 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));
});

  1. 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).
  2. 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 interface IFooand created between memoryCheckPoint1and memoryCheckPoint2does not exceed 1000 bytes.
  3. You can receive data on any of the previously created checkpoints. So, this line requests traffic data between the current call dotMemory.Checkand memoryCheckPoint2.


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.

Read Next