In the current project I’m working on, we are intensively writing unit tests with 100% code coverage following TDD.
In this post I’m going to share one of very exciting situations that I faced couple of days ago while working on a module which is supposed to initialise database on fist access.
While the first caller is waiting for the database to be initialised, the second caller needs to wait as well and do not trigger the initialise method again. After the wait is over the second caller needs to know that the database is initialised and continue the normal work.
The fact of putting the initialisation in the database client is not the centre of this post.
There are couple of ways of tackling this but IMO the best way is to actually make a race situation happen in your unit test. Some developers do this by firing an action and delaying that by putting the first thread to sleep and fire the second thread while the first thread is asleep. The biggest problem is that how long do you want to put the first thread to sleep. On different machine the process of running the unit tests in the test runner takes different time to run. So you should either introduce a very long delay (seconds) or a short one that runs on your machine but may fail on the build server.
Here is how I solved this issue:
I used two indefinite while loops. I know it sounds very ugly but hey it’s unit test and not the implementation of the service itself. By using the loops I’m simply letting the thread asleep to be variable at the minimum that ensures the race situation happens.
I let the second thread to start in the loop of the first thread and then let the first thread to finish in the loop of the second thread. That way the second call is always guaranteed to hit the race situation.
Please note that I have injected my own mocked Semaphore so I can handle the checks without using fakes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
... public bool IsInitialized { get; private set; } public async Task EnsureInitialized(IDocumentServiceConfig config) { if (IsInitialized) { return; } // The first actor should get the lock while the second actor will // wait for the lock to be released await _semaphore.WaitAsync(); if (IsInitialized) { _semaphore.Release(); return; // the second actor will return here } // the first actor will init the database if (!config.InitializeDatabaseOnStart) { IsInitialized = true; _semaphore.Release(); return; } try { await InitialiseDatabase(config); IsInitialized = true; } finally { _semaphore.Release(); } } ... |
- xUnit
- NSubstitute
- NFluent
Here is the unit test in two ways:
Method 1: Mocking the Semaphore and handling the wait and release in order to make sure the race condition happens:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
... [Fact] public void EnsureInitialized_WillNotAllowMultipleCallsAtTheSameTime_ByLocking() { var mockedService = NSubstituteHelper.Substitute<DocumentDbInitializerService>(); var service = mockedService.Target; var semaphore = mockedService.Get<ISemaphoreSlim>(); var config = Substitute.For<IDocumentServiceConfig>(); var callCount = 0; var configCallCount = 0; var firstCallIsMade = false; var secondCallIsMade = false; var secondCallTriedToInitializeDatabase = false; var canLock = true; semaphore.WaitAsync().Returns(info => { callCount++; if (callCount == 1) { while (!secondCallIsMade) { Thread.Sleep(10); // avoid CPU load firstCallIsMade = true; } } else if (callCount >= 2) // assert { secondCallIsMade = true; } while (!canLock) // the second call will wait until the first call is released { Thread.Sleep(10); } if (canLock) canLock = false; return Task.CompletedTask; }); semaphore.Release().Returns(info => { canLock = true; return 0; }); config.InitializeDatabaseOnStart.Returns(info => { configCallCount++; if (configCallCount >= 2) // for assertion { secondCallTriedToInitializeDatabase = true; // should fail the test } return false; }); Task.Factory.StartNew(() => service.EnsureInitialized(config)); // act while (!firstCallIsMade) // waiting for the first call to be made { Thread.Sleep(10); // avoid CPU load } service.EnsureInitialized(config).GetAwaiter().GetResult(); // assert Check.That(secondCallTriedToInitializeDatabase).IsFalse(); } ... |
Method 2: Inject a semaphore and let it do its job while we force the racing situation to happen using only the config:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
... [Fact] public void EnsureInitialized_WillNotAllowMultipleCallsAtTheSameTime_ByLocking() { // arange var service = new DocumentDbInitializerService( semaphore: new SemaphoreSlimWrapper(1, 2) loggingService: Substitute.For<ILoggingService>(), documentClient: Substitute.For<IDocumentClient>()); var config = Substitute.For<IDocumentServiceConfig>(); var callCount = 0; var shouldWait = true; var firstCallIsMade = false; var secondCallTriedToInitializeDatabase = false; config.InitializeDatabaseOnStart.Returns(info => { callCount++; if (callCount == 1) // first call { // ReSharper disable once AccessToModifiedClosure while (shouldWait) { Thread.Sleep(10); // avoid CPU load firstCallIsMade = true; } } else if (callCount >= 2) // second call { secondCallTriedToInitializeDatabase = true; // should fail the test } return false; }); Task.Factory.StartNew(() => service.EnsureInitialized(config)); // act while (!firstCallIsMade) // waiting for the first call to be made { Thread.Sleep(10); // avoid CPU load } shouldWait = false; service.EnsureInitialized(config).GetAwaiter().GetResult(); // assert Check.That(secondCallTriedToInitializeDatabase).IsFalse(); } ... |