PVS-Studio and NUnitДовольно часто при обсуждении средств статического анализа для C# проектов программисты пишут о том, что в этом нет необходимости, потому что с помощью юнит-тестирования они отлавливают большинство ошибок. Я решил проверить, насколько хорошо протестирован один из самых известных юнит-тест фреймворков — NUnit, и посмотреть найдёт ли там что-нибудь наш анализатор.

Введение


NUnit — это портированная с Java на C# популярная библиотека для юнит-тестирования .NET проектов. Исходный код открыт и доступен на сайте проекта http://www.nunit.org/.

Стоит отметить, что JUnit — проект, с которого был портирован NUnit, создали такие известные программисты как Эрих Гамма — один из авторов книги о шаблонах объектно-ориентированного проектирования, и Кент Бек — создатель методологий разработки через тестирование и экстремального программирования. Я помню, как когда-то читал его книгу Test Driven Development By Example, где он рассказывает о разработке через тестирование на примере создания тестового фреймворка, аналогичного JUnit, следуя всем своим методологиям. То есть можно не сомневаться в том, что JUnit и NUnit разработаны в лучших традициях юнит-тестирования, о чём так же говорит цитата Кента Бека на сайте NUnit: "… великолепный пример идиоматического дизайна. Большинство тех, кто портировал xUnit просто переносили версию для Smalltalk или Java. То же самое сделали и мы с NUnit в первый раз. Эта новая версия NUnit является такой, какой она должна была быть, если бы была написана на C# с самого начала".

Я посмотрел исходники NUnit — там очень много тестов, ощущение такое, что протестировано всё что только можно. Учитывая отличный дизайн и тот факт, что NUnit используется тысячами разработчиков на протяжении многих лет, я думал, что PVS-Studio не найдёт в нём ни одной ошибки. Но нет, ошибка нашлась.

О найденной ошибке


Сработала диагностика V3093 о том, что иногда программисты вместо операторов && и || используют операторы & и |. Проблема может возникнуть в том случае, когда важно чтобы правая часть выражения не выполнялась при определённых условиях. Посмотрим, как эта ошибка выглядит в NUnit.

public class SubPathConstraint : PathConstraint
{
    protected override bool Matches(string actual)
    {
        return actual != null &
            IsSubPath(Canonicalize(expected), Canonicalize(actual));
    }
}
public abstract class PathConstraint : StringConstraint
{
    protected string Canonicalize(string path)
    {
        if (Path.DirectorySeparatorChar !=
            Path.AltDirectorySeparatorChar)
            path = path.Replace(Path.AltDirectorySeparatorChar,
                                Path.DirectorySeparatorChar);
        ....
    }
}

Даже если в метод Matches в качестве параметра actual придёт значение null, то правая часть оператора & всё равно будет вычислена, а значит будет вызван метод Canonicalize. Если посмотреть его определение, то видно, что в нём значение параметра path уже не проверяется на null, а сразу у него зовётся метод Replace, где и возможен потенциальный NullReferenceException. Попробуем воспроизвести проблему. Для этого я написал простой юнит-тест:

[Test]
public void Test1()
{
    Assert.That(@"C:\Folder1\Folder2", Is.SubPathOf(null));
}

Запускаем и смотрим результат:

NUnit NullReferenceException

Так и есть: NUnit упал с NullReferenceException. Даже в таком хорошо протестированном продукте как NUnit статический анализатор PVS-Studio смог найти реальную ошибку. Отмечу, что сделать это мне было не сложнее, чем написать юнит-тест — нужно выбрать из меню проверку проекта и проанализировать грид с результатами.

Заключение


Юнит-тесты и статический анализ — это не исключающие, а дополняющие друг друга методики разработки программного обеспечения [1]. Скачайте статический анализатор PVS-Studio и посмотрите не обнаружатся ли там ошибки, которые не были найдены юнит тестами.

Дополнительные ссылки


  1. Андрей Карпов. Как статический анализ дополняет TDD.
  2. Илья Иванов. Об одной интересной ошибке в Lucene.Net.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Alexander Chibisov. Complementing Unit Testing with Static Analysis, with NUnit as an Example.

Прочитали статью и есть вопрос?
Часто к нашим статьям задают одни и те же вопросы. Ответы на них мы собрали здесь: Ответы на вопросы читателей статей про PVS-Studio, версия 2015. Пожалуйста, ознакомьтесь со списком.
Поделиться с друзьями
-->

Комментарии (8)


  1. potan
    17.08.2016 15:01
    +1

    Кстати, о юнит-тестировании.
    Статический анализ мог бы находить мест в коде не покрытые тестами. Вы этим занимаетесь?


    1. Andrey2008
      17.08.2016 15:31
      -1

      Прошу переформулировать вопрос, так как сейчас не знаю что ответить, кроме как «Естественно, да». :)


      1. Andrey2008
        17.08.2016 15:42

        Ааа. Я понял. Я неправильно ответил. Я думал про «находить места с ошибками в коде».

        Если имеется в виду поиск кода, непокрытого тестами — то нет.


      1. potan
        17.08.2016 15:46
        +1

        Статическому анализатору скармливаются исходники программы и юнит-тестов, а он отвечяет — такая-то строка кода в процессе тестов не выполняется и к такому-то элементу статически инициализированного массива не обращаются.


        1. Andrey2008
          17.08.2016 15:49

          Нет, такое мы не делаем.


        1. j8kin
          17.08.2016 16:46
          +1

          В идеальном мире, где нет времени статический анализатор может дать такую картину.
          Там где есть время, статический анализатор будет давать ложные покрытия кода.
          Без запуска никак не обойтись, но тут тоже проблеммы есть. Так как приходится инструментировать код, то он «распухает» итоговый экзешник обычно становится на 120% больше (в зависимости от уровня покрытия кода). Это заметно увеличивает время исполнения этого кода и может получится так, что код без инструментализации выполняет определенную ветку кода, а с инструментализацией уже не успевает. Тут приходится много думать, как это исправить и возможно делить тест или стабировать больше исходного кода.
          Но опять же статический анализатор тут не поможет ибо он не покажет как это будет работать на реальном железе в реальном времени.
          Я бы даже сказал он будет тут вреден ибо он покажет, что все выполнится и все ок, а на самом деле это может оказаться далеко не так и как результат дорогостоящие переделки кода и аппаратного обеспечения.


    1. j8kin
      17.08.2016 15:41
      +2

      Этим занимаются совершенно иные Инструментальные Средства, например Bullseye. Есть разные уровни покрытия кода и свои фичи связанные с тем, как в итоге исходный код транслируется в объектный. Статическим Анализом можно лишь предположить некоторое покрытие/не покрытие, но оно может быть далеко от реального покрытия.


    1. EvgeniyRyzhkov
      17.08.2016 15:46
      +2

      Code Coverage — другая задача. Не умеем.