NDepend Blog

Improve your .NET code quality with NDepend

How we quickly refactored with Resharper more than 23.000 calls to Debug.Assert() into more meaningful assertions

January 11, 2021 6 minutes read

Since the NDepend inception more than 15 years ago, we stuffed our code with calls to Debug.Assert(). This results today in more than 23.000 assertions calls.

Few developers realize that assertions in code are as important as assertions in tests. In both cases they prevent corrupted states at both smoke test-time and automatic test-time. Smoke testing means when you manually test your application. It is like smoke: your effort into testing manually is lost once done.

The scientific name for assertion in code is Design by Contract. Ok, contracts integrated in a language are more powerful than raw assertions, especially concerning inheritance, but the ideas are quite similar.

Our code was stuffed with 23.000 calls to Debug.Assert() and we wanted to improve that:

  • To use more fluent and modern assertions like Assert.IsNotNull(obj) instead of Debug.Assert(obj != null)
  • To be able to get some AssertionException at test-time instead of a blocking assertions dialog
  • To eventually have a Release version that throws AssertionException. As its name suggests calls to Debug.Assert() are removed in the Release version.

This task might appears daunting due to the large number of calls. However thanks to the feature Resharper > Find > Replace with Patterns feature, it took just half a day to complete. In this post I would like to highlight this powerful feature and our case-study. Hopefully you’ll adapt it to your own refactoring scenarios.

Defining our own assertion library

There are many open-sources assertions libraries. Here we’d like to stick with a basic NUnit-like approach. Hence it is not complicated to define a small library fully adapted to our needs like:

A few remarks:

  • We name our assertion class MyAssert and it is declared in the System.Diagnostics namespace. Since the Debug class is also declared in the System.Diagnostics namespace changing calls to Debug to MyAssert don’t provoke compilation errors. Also our tests already used a class named Assert so having our own MyAssert class avoid ambiguous calls. Both MyAssert and System.Diagnostics identifiers can be temporary. All the 23K calls to MyAssert can be changed through a single refactoring action after the major refactoring session.
  • We can choose between a Debug.Assert() style or a throw new AssertionException() style depending on the running context.
  • [Conditional(“DEBUG”)] attributes strip off all assertions calls from the release version. Executing assertions consume performance so we prefer a super fast Release version and have our own strategies to decipher production crashes logged (stategies explained in Toward Bug Free Software: Lines of Defense).
  • If needed, we can still comment all [Conditional(“DEBUG”)] and send release version with assertions to a user that experiences a bug.
  • We use generic methods to be able to differentiate between assertions on references or assertion on values. As explained just below, this will let the compiler detect some refactoring issues for us.

Doing the Refactoring

First click Resharper >Search >Find with Pattern:

Resharper > Find > Search with Pattern

Below, for example, we choose the search pattern Debug.Assert($var1$ != null); and the replace pattern MyAssert.IsNotNull($var1$);. Note that you need to select the Replace button in the top-right corner of the dialog. Else you only get the search feature.

Resharper: Search with Pattern Dialog

Then we can preview the 16.475 calls that will be refactored. We can eventually discard some calls thanks to the checkboxes.

Resharper: Replace with Pattern Preview Dialog

A few remarks:

  • Calls to Debug.Assert(val != null); where val is a nullable value (like int?) will be refactored to MyAssert.IsNotNull(val) instead of MyAssert.IsNotNullValue(val). The compiler will then emit some errors since the IsNotNull<T>() method forces T to be a reference type. Hopefully this concerns only a few dozens of call that can be handled manually once spotted by the compiler.
  • We’re not using yet the awesome C#8 non-nullable reference feature. When we’ll refactor our code to support this feature Resharper > Replace with Patterns won’t be powerful enough. However Resharper can help also with migration to nullable reference types. But having all those 16K assertions will be precious to determine which reference can be null or not. Maybe writing our own refactoring program based on Roslyn guided by our numerous calls to MyAssert.IsNotNull() will be the best approach. I’ll blog on this when we’ll be there.
  • With the pattern Debug.Assert($var1$ != null); Resharper is smart enough to also handle reversed cases like Debug.Assert(null != x);.
  • Replacing thousands of expressions in thousands of files is an expensive operations. Resharper freezes Visual Studio during several minutes. I wish Resharper could propose a progress dialog with eventually a Cancel button. So far we didn’t experience any crash even when replacing 16K expressions.

Non-trivial Refactoring

Refactoring expressions like Debug.Assert(obj1 == obj2) is not trivial. Indeed if the type of both objects is a reference type that overloads the equality/inequality operators refactoring such expression to MyAssert.ReferenceEquals(obj1,obj2) won’t work. You might think that it is a rare case but actually System.String compare strings by value! Hopefully we can match only expressions typed as string:

Smart detection of string equality

Other reference types overriding the equality and inequality operators like…

…can be found with this NDepend code query:

matching overloads of equality operator with a ndepend query

We don’t use equality operator overloading because it is wise to reserver this feature to types like BigInteger, Vector, Matrix… . Hopefully this query tells us to take care of BCL types like System.Version and System.Delegate. Of course now with C#9 records many classes will be compared by value. However we don’t record yet in our code.

As before, replacing to MyAssert.ReferenceEquals($var1$,$var2$) will emit some compiler errors when the underlying type is a value type. We don’t provide a MyAssert.ValueEquals(val1,val2) method. Here we refactor manually to MyAssert.IsTrue(val1 == val2) because value type comparison can be tricky. To illustrate this complexity, we allow comparing Debt value with TimeSpan value. This is because a technical-debt estimation is expressed in cost-to-fix which is measured by human-time.

matching overloads of equality operator on struct with a ndepend query

From there we don’t need to detail other assertion refactoring because hopefully you now get a sense on how to use this feature on both trivial and non-trivial situations.

Conclusion

Thanks to the Resharper Structural Search and Replace feature, we’ve been able to refactor 23.000 calls within half a day. As explained there have been some edge cases and sometime we had to rely on compilation errors. But without this feature we would have had to write our own program (or analyzer) based on Roslyn.

In a 2020 post I explained the top 10 Visual Studio 2019 refactoring actions. I got many thankful feedback from developers that didn’t know that Visual Studio was catching up Resharper on the refactoring field. However this Structural Search & Replace feature could justify alone a Resharper license. This is the kind of feature for which one can regularly imagine use-cases. For example it also works on .aspx code and we used it a lot to refactor and improve substantially the technical SEO of our website.

structural search and pattern works on aspx code

The other part of the blog is about assertions in code. Once again I underline that this is a common misbelief that assertions should be reserved to test code only. Assertions in code is a major weapon to both express your intention and detect bugs early. Also in many situations it can simplify testing. For example when testing UI, typically it is a pain to find relevant states to assert in  test code. But if your UI code is stuffed with assertions, the tests just need to pilot the UI and assertions in code are then checked at test-time. For example here is a video of our test piloting the NDepend Graph UI extracted from the post Case Study : Complex UI Testing. Many time this test broke some assertions in code and helped us detect bugs early.