I basically ended up doing what @Alexei said. To be more specific, this is what I did:
- Make a
PausingDictionary
which basically just has a callback for every method, but otherwise just passes through to a regular dictionary
- Abstract my code(using DI, etc) so that I can use PausingDictionary instead of a regular Dictionary when testing
- Added two ConcurrentDictionaries in my unit test. One called "Accessed" and one called "GoAhead". The key to thiis is a combination of
"action"+Thread.GetHashCode().ToString()
(where action is different for each callback)
- Initialized everything to false and added some extension methods to make working with it a bit easier
- Setup the dictionary's callbacks to set Accessed for the thread to true, but it would then wait in the callback until GoAhead was true
- Started two threads from within the unit test. One thread would access dictionary, but because
GoAhead
is false for that thread, it'd sit there. The second thread would then also attempt to access the dictionary
- I'd have an assertion that
Accessed
for that thread is false, because my code should lock it out.
There's a bit more to it than that. I'd also need to mock up an IList, but I don't think I will. These unit tests, while valuable, are definitely not the easiest thing in the world to write. Aside from setup code and fake interface implementations and such, each test ends up being about 25 lines of not-boilerplate code. Locking is hard. Proving that your locking is effective is even harder. Amazingly though, this kind of pattern can allow you to test almost any scenario. But, it's very verbose and does not make for pretty tests
So, despite it being hard to write the tests, this works perfectly. When I remove a lock is consistently fails and when I add back the lock, it consistently passes.
Edit:
I think this method of "controlling interleave" of threads would also make it possible to test thread-safety, given that you write a test for each possible interleave. With some code this would be impossible, but I just want to say this is in no way limited to only locking code. You could do the same way to consistently duplicate a thread-safe failure like foo.Contains(x)
and then var tmp=foo[x]