{"id":49113,"date":"2023-11-21T10:05:00","date_gmt":"2023-11-21T18:05:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=49113"},"modified":"2023-12-01T08:13:36","modified_gmt":"2023-12-01T16:13:36","slug":"fake-it-til-you-make-it-to-production","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/fake-it-til-you-make-it-to-production\/","title":{"rendered":"Fake It Til You Make It&#8230;To Production"},"content":{"rendered":"<p>A few years back, I started an internal Microsoft project called R9. Our\ngoal was to create a robust SDK to accelerate the development of\nreliable high-scale services on .NET. Hundreds of microservices within\nMicrosoft now leverage this SDK and happily many of the technologies we\ncreated have been folded into .NET 8 for the benefit of the entire .NET\necosystem.<\/p>\n<p>We knew starting the R9 project that asking service teams to adopt\nour technology would be challenging. Teams have a very high quality bar\nand if we didn&#8217;t meet that bar, it might\nbe a long time before they gave us another look, if ever. We therefore\nput a strong emphasis on quality, establishing a target of 100% code\ncoverage, embracing <a href=\"https:\/\/stryker-mutator.io\/\">mutation testing<\/a>, and\nadopting a zero-bug policy.<\/p>\n<h2>Why we Started Faking It<\/h2>\n<p>Since the R9 components generate a fair bit of telemetry, we decided it\nwas critical for us to test the quality of our telemetry. Service teams\ndepend on having the right signals coming from their applications to be\nable to successfully deploy, monitor, and repair their services. We\nneeded to make sure our telemetry was part of the solution, not the\nproblem.<\/p>\n<p>At first, we were timidly using generated mocks\nto capture telemetry from our components in ad hoc ways. Although this\ntechnically worked, it was ugly. Using this approach usually requires tests\nto assume the internal structure of the components being tested, which\nmakes tests brittle as they become susceptible to break when the internal\nstructure of a component is refactored. Since it was clumsy to write, we\nended up with few tests validating telemetry throughout our code base.<\/p>\n<p>After a while, it became clear that to achieve our quality objectives,\nwe would need to improve how telemetry is tested, which led me to\ncreate the logging and metric fakes. The model was successful as R9\ndevelopers quickly embraced it and the quality of our telemetry tests\nimproved dramatically. The fakes model caught on within the team, we\ncreated several other fakes to help improve both our tests and our\ncustomer\u2019s tests.<\/p>\n<h2>Time Is a Problem<\/h2>\n<p>Anybody that\u2019s been part of a large software engineering team knows the\ninherent problems caused by flaky tests. Flaky tests are those that\ngenerally work, but occasionally trigger a false negative. If you have\nthousands of test cases, a handful of flaky ones can make it difficult\nto have a fully successful test run, which in turn makes it difficult to\nknow if changes you\u2019re making to the code base are causing test\nfailures, or it\u2019s merely a flaky test doing mischief.<\/p>\n<p>Most flaky tests are caused by some sort of race condition, and often\nthese races exist purely in the tests and not in the software under\ntest. This happens because the environment in which software is tested\nis often hacked together, being different than how the software is\nnormally used.<\/p>\n<p>To help tame the race conditions, I decided to create an <code>IClock<\/code>\nabstraction. We\u2019d use this throughout our SDK to do anything time\nrelated. <code>IClock<\/code> let us use a fake clock in our tests to completely\ncontrol the passage of time. Once deployed and used, this dramatically\nreduced the number of flaky tests in our code base.<\/p>\n<p>At some point, we searched through a bunch of internal Microsoft code\nbases and found literally hundreds of variations of <code>IClock<\/code> and\nassociated types to use in testing. Clearly, this was a common problem\nthat deserved some attention. This work ultimately led to the\nintroduction of the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.timeprovider\"><code>TimeProvider<\/code><\/a>\ntype in .NET 8.<\/p>\n<h2>Fakes in .NET 8<\/h2>\n<p>.NET 8 introduces several fakes to simplify test authoring. In this\narticle, I\u2019ll describe the logging fake, the metering fake, and the time\nprovider fake. We don\u2019t have the space to cover those here, but be on\nthe lookout for other useful fakes such as the fake host, fake\nredactor, and fake data taxonomy.<\/p>\n<h2>Logging Fake<\/h2>\n<p>The idea behind the logging fake is to use a custom implementation of\n<a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/microsoft.extensions.logging.ilogger\"><code>ILogger<\/code><\/a>\nwhose job is to capture and accumulate all state being logged in\nan in-memory buffer such that it can be inspected from a test suite.<\/p>\n<p>You&#8217;ll find the logging fake in the <a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.Extensions.Diagnostics.Testing\">Microsoft.Extensions.Diagnostics.Testing<\/a> package.<\/p>\n<p>Getting started is easy, you can just create a <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/microsoft.extensions.logging.testing.fakelogger\"><code>FakeLogger<\/code><\/a>\ninstance and pass it to any code that is asking for an <code>ILogger<\/code> or <code>ILogger&lt;T&gt;<\/code>:<\/p>\n<pre><code class=\"language-csharp\">[Fact]\npublic static void TestLogging()\n{\n    \/\/ a plain logger\n    var logger1 = new FakeLogger();\n    var c1 = new ClassToTest1(logger1);\n\n    \/\/ a logger with a specific logging category\n    var logger2 = new FakeLogger&lt;ClassToTest2&gt;();\n    var c2 = new ClassToTest2(logger2);\n}<\/code><\/pre>\n<p>Sometimes, code wants an <code>ILoggerProvider<\/code> out of which it creates its own\nlogger. For those cases, you can use the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/microsoft.extensions.logging.testing.fakeloggerprovider\"><code>FakeLoggerProvider<\/code><\/a>\ntype either directly or through dependency injection.<\/p>\n<p>Once you\u2019ve got a logger provider or logger wired-in, anything logged\nwill be captured in a <code>FakeLogCollector<\/code> object. If you manually created\nthe logger, you can get the collector via the <code>FakeLogger.Collector<\/code>\nproperty. If you used <code>FakeLoggerProvider<\/code> instead and you don\u2019t have\nimmediate access to the logger object in your code, you can then use the\n<code>GetFakeLogCollector<\/code> method to extract the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/microsoft.extensions.logging.testing.fakelogcollector\"><code>FakeLogCollector<\/code><\/a>\nused by the logger instances created by dependency injection.<\/p>\n<p>Once you have the <code>FakeLogCollector<\/code> object, you can do one of these\nthings:<\/p>\n<ul>\n<li>\n<p>Check the <code>Count<\/code> property to make sure the expected number of log\nrecords were produced.<\/p>\n<\/li>\n<li>\n<p>Call <code>GetSnapshot<\/code> to get a copy of all the log records collected in the\ntest case.<\/p>\n<\/li>\n<li>\n<p>Use the <code>LatestRecord<\/code> property to get access to the last log record\nthat was collected.<\/p>\n<\/li>\n<\/ul>\n<p>The most common pattern is to check that <code>Count<\/code> equals 1, and then\nuse <code>LatestRecord<\/code> to ensure the logged data is correct. The <code>FakeLogRecord<\/code>\nclass contains properties to let you access all the state captured with\nevery log record including the log level (debug, warning, error, etc),\nthe category, the message, and the full set of name\/value tags.<\/p>\n<pre><code class=\"language-csharp\">[Fact]\npublic static void TestLogging()\n{\n    \/\/ create a fake logger for this test\n    var logger = new FakeLogger();\n\n    \/\/ pass the fake logger in to the code being tested\n    var c = new ClassToTest1(logger);\n\n    \/\/ trigger the code to do some work\n    c.DoSomethingThatFails();\n\n    \/\/ verify that the DoSomethingThatFails method has logged what it should have\n\n    \/\/ only one log record\n    Assert.Equal(1, logger.Collector.Count);    \n\n    var r = logger.LatestRecord;\n\n    \/\/ should be logged as an error\n    Assert.Equal(LogLevel.Error, r.Level);\n\n    \/\/ the message should include the text \u201cCould not do something\u201d\n    Assert.Contains(\"Could not do something\", r.Message);\n\n    \/\/ there should be a \u201ccause\u201d tag with the value \u201cno_support\u201d\n    Assert.Contains(r.StructuredState, tag =&gt; tag.Key == \"cause\" &amp;&amp; tag.Value == \"no_support\"); \n}<\/code><\/pre>\n<h2>Metric Fake<\/h2>\n<p>The metric fake uses a similar model of capturing all the reported\nmetric updates and making it easy for you to inspect the resulting\nstate. Since metrics integrate very differently into .NET than logging\ndoes, the fake model is also different and simpler.<\/p>\n<p>You&#8217;ll find the metering fake in the <a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.Extensions.Diagnostics.Testing\">Microsoft.Extensions.Diagnostics.Testing<\/a> package.<\/p>\n<p>The <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/microsoft.extensions.diagnostics.metrics.testing.metriccollector-1\"><code>MetricCollector&lt;T&gt;<\/code><\/a> object lets you record all updates to a metric\ninstrument or observable instrument. Once you\u2019ve created the collector,\nyou can use the <code>LastMeasurement<\/code> property to query the last update to the\ninstrument or call <code>GetMeasurementSnapshot<\/code> to get a list of all the\nmeasurements captured for the instrument.<\/p>\n<p>There are several different constructors for the <code>MetricCollector<\/code> class,\neach letting you specify the specific instrument to collect from in a\ndifferent way, which tailors to most use cases.<\/p>\n<p>Here\u2019s an example showing how to use the <code>MetricCollector<\/code> type:<\/p>\n<pre><code class=\"language-csharp\">public sealed class ClassToTest : IDisposable\n{\n    private const string CounterName = \"MyCounter\";\n    private readonly Meter _meter;\n\n    public ClassToTest()\n    {\n        _meter = new Meter(Guid.NewGuid().ToString());\n        Counter = _meter.CreateCounter&lt;long&gt;(CounterName);\n    }\n\n    public void Dispose() =&gt; _meter.Dispose();\n\n    public void DoWork(string workType, int unitsOfWork)\n    {\n        Counter.Add(unitsOfWork,\n            new KeyValuePair&lt;string, object?&gt;[]\n            {\n                new(\"workType\", workType),\n            });\n     }\n\n    internal Counter&lt;long&gt; Counter { get; }\n}\n\n[Fact]\npublic static void TestMetering()\n{\n    const string WorkType = \"Bootstrap\";\n    const int WorkUnits = 3;\n\n    using var classToTest = new ClassToTest();\n    using var collector = new MetricCollector&lt;long&gt;(classToTest.Counter);\n\n    \/\/ show the collector has no state\n    Assert.Empty(collector.GetMeasurementSnapshot());\n    Assert.Null(collector.LastMeasurement);\n\n    \/\/ now do some work that should update the counter\n    classToTest.DoWork(WorkType, WorkUnits);\n\n    \/\/ now verify the right telemetry was updated\n    var snap = collector.GetMeasurementSnapshot();\n    Assert.Equal(1, snap.Count);\n\n    \/\/ get the sole measurement taken\n    var m = snap[0];\n\n    \/\/ make sure the value is what it should be\n    Assert.Equal(WorkUnits, m.Value);\n\n    \/\/ make sure the tags are exactly what they should be\n    Assert.True(m.MatchesTags(new KeyValuePair&lt;string, object?&gt;(\"workType\", WorkType)));\n\n    \/\/ make sure the exact set of tags are present, regardless of the specific values they carry\n    Assert.True(m.MatchesTags(\"workType\"));\n\n    \/\/ make sure at least the given tags are present\n    Assert.True(m.MatchesTags(new KeyValuePair&lt;string, object?&gt;(\"workType\", WorkType)));\n\n    \/\/ make sure at least the given tags are present, regardless of the specific values they carry\n    Assert.True(m.ContainsTags(\"workType\"));\n}<\/code><\/pre>\n<h2>TimeProvider Fake<\/h2>\n<p>The <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.timeprovider\"><code>TimeProvider<\/code><\/a> type is new to .NET 8 and it provides a simple set of\nmethods to get the current time, get the current time zone, and create\ntimers. This is like functionality already present in .NET, except for\nthe fact the methods are not static. Instead, they are instance methods\non a type that\u2019s designed to have both real and fake implementations.<\/p>\n<p>The really useful aspect of <code>TimeProvider<\/code> is that it\u2019s been integrated\ninto core functionality of the framework. For example, there are now\nconstructors for <code>CancellationTokenSource<\/code> and <code>PeriodicTimer<\/code> which take a\nprovider, <code>Task.WaitAsync<\/code> and <code>Task.Delay<\/code> now have overloads that take a\nprovider.<\/p>\n<p>If you provide a fake time provider in all those places, you can\ncompletely control how time behaves in those APIs. So instead of having\nrace conditions all over your test suite, you can have fully\ndeterministic tests that execute much faster than before (since you can\navoid the nasty delays that typically get added to test suites to work\naround the race conditions).<\/p>\n<p>The fake time provider is quite different than the logging and metric\nfakes we just discussed. Instead of recording events, the fake time\nprovider is there as a manually controlled clock. Whereas the system\nprovider, which you get from <code>TimeProvider.System<\/code>, tracks real-world\ntime, the apparent passage of time is instead entirely controlled by the\nfake time provider.<\/p>\n<p>You&#8217;ll find the time provider fake in the <a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.Extensions.TimeProvider.Testing\">Microsoft.Extensions.TimeProvider.Testing<\/a> package.<\/p>\n<p>The <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/microsoft.extensions.time.testing.faketimeprovider\"><code>FakeTimeProvider<\/code><\/a> type lets you do the following special things:<\/p>\n<ul>\n<li>\n<p>You can set the specific time that the provider returns to callers.<\/p>\n<\/li>\n<li>\n<p>You can increment the specific time that the provider returns to\ncallers.<\/p>\n<\/li>\n<li>\n<p>You can set an amount by which the specific time that the provider\nreturns is automatically incremented any time the provider\u2019s time is read.<\/p>\n<\/li>\n<\/ul>\n<p>Controlling the provider\u2019s time is like a superpower. If your code calls\n<code>Task.Delay<\/code> with a fake provider and a delay of 1 second, <code>Task.Delay<\/code> will\nnot return until the provider\u2019s time is manually advanced to 1 second.\nThis can in fact take 1 microsecond or 1 hour of real-world time, that\nno longer matters.<\/p>\n<pre><code class=\"language-csharp\">public sealed class KeepAlive\n{\n    private readonly TimeProvider _timeProvider;\n    private readonly ILogger _logger;\n\n    \/\/ the normal public constructor which uses the system time provider and so is subject to real-time\n    public KeepAlive(ILogger logger)\n        : this(logger, TimeProvider.System)\n    {\n    }\n\n    \/\/ an internal constructor used by tests to supply a fake time provider to control the passage of time\n    internal KeepAlive(ILogger logger, TimeProvider timeProvider)\n    {\n        _logger = logger;\n        _timeProvider = timeProvider;\n\n        \/\/ start a background task that logs a message every second\n        _ = Task.Run(async () =&gt;\n        {\n            await Task.Delay(TimeSpan.FromSeconds(1), _timeProvider);\n            _logger.LogInformation(\"I'm Alive!\");\n        });\n    }\n}\n\n[Fact]\npublic async Task TestTime()\n{\n    var logger = new FakeLogger();\n    var timeProvider = new FakeTimeProvider();\n    var classToTest = new KeepAlive(logger, timeProvider);\n\n    \/\/ wait 2 real-world seconds\n    await Task.Delay(2000);\n\n    \/\/ confirm that nothing has been logged yet\n    Assert.Equal(0, logger.Collector.Count);\n\n    \/\/ now advance the clock by almost a whole second, but not quite\n    timeProvider.Advance(TimeSpan.FromMilliseconds(999));\n\n    \/\/ wait 2 real-world seconds\n    await Task.Delay(2000);\n\n    \/\/ confirm that nothing has been logged yet\n    Assert.Equal(0, logger.Collector.Count);\n\n    \/\/ now advance the clock by another millisecond\n    timeProvider.Advance(TimeSpan.FromMilliseconds(1));\n\n    \/\/ wait 2 real-world seconds\n    await Task.Delay(2000);\n\n    \/\/ confirm that one thing has been logged\n    Assert.Equal(1, logger.Collector.Count);\n}<\/code><\/pre>\n<p>As you can see, until the fake time provider\u2019s time is explicitly\nadvanced by the test, the <code>Task.Delay<\/code> call in the <code>KeepAlive<\/code> class just sits\nthere and blocks.<\/p>\n<p>Using this general model lets you finely control what happens in any\ntime-sensitive code, turning hard-to-test non-determinism into\nrock-solid deterministic logic.<\/p>\n<h2>Summary<\/h2>\n<p>I hope this has given you a taste of what it\u2019s like to test with the new\nfunctionality provided in .NET 8. These features are designed to make it\neasier to write robust tests, which in turn should make it faster to get\nyour code into production.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Explores the new logging, metric, and time provider fakes introduced in .NET 8<\/p>\n","protected":false},"author":100398,"featured_media":49384,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685],"tags":[7701,7768,7693,7767],"class_list":["post-49113","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","tag-dotnet-8","tag-aspire","tag-cloud-native","tag-r9"],"acf":[],"blog_post_summary":"<p>Explores the new logging, metric, and time provider fakes introduced in .NET 8<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/49113","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/100398"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=49113"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/49113\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/49384"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=49113"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=49113"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=49113"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}