Насмешка над классом как с чистыми методами, так и с побочными эффектами - PullRequest
0 голосов
/ 28 февраля 2019

Предположим, у нас есть этот интерфейс:

public interface IFileSystem
{
    string ReadFile(string filename);
    string CombinePaths(string path1, string path2);
}

и следующая конкретная реализация

public class ConcreteFileSystem : IFileSystem
{
    public string CombinePaths(string path1, string path2)
    {
        return path1 + "/" + path2;
    }
}

Реализация ReadFile здесь не важна.

НаличиеЭкземпляр файловой системы очень хорош, поскольку он позволяет имитировать вызовы файловой системы, которые имеют побочные эффекты (например, ReadFile).

Затем у нас есть тест

var mock = new Mock<IFileSystem>();
var fs = mock.Object;

// How do I forward the calls to Mock<IFileSystem>.CombinePaths to ConcreteFileSystem.CombinePaths?

Assert.IsTrue(SomeClass.SomeMethodThatUsesCombinePaths("specificString1"))

То, что в данный момент выполняетсяв моей базе кода делается такой код в каждом тесте:

mock.Setup(f => f.CombinePaths("specificString1", "specificString2"))
    .Returns("specificString1/specificString2");

Но поскольку тесты должны тестировать интерфейс, а не реализацию, это кажется плохим подходом.Кроме того, при дублировании некоторого тестового кода могут появляться незначительные ошибки.


Я думал только о

mock.Setup(f => f.CombinePaths(It.IsAny<string>(), It.IsAny<string>()))
    .Returns<string, string>((path1, path2) => new FileSystem().CombinePaths(path1, path2));

(возможно, это можно сократить с помощью некоторого синтаксиса, специфичного для C #).

Такой код в разделе [SetUp] тестов для каждого метода IFilesystem больше не зависит от реализации SomeClass.SomeMethodThatUsesCombinePaths.

У меня вопрос, является ли этохороший подход или как этот подход может быть улучшен.Может быть, есть более фундаментальный способ действовать по-другому.

Ответы [ 2 ]

0 голосов
/ 01 марта 2019

Интересно, почему ты вообще издеваешься? CombinePaths.Насмешка должна быть сделана по причине.На это есть веские причины:

  • Вы не можете легко перевести зависимый компонент (DOC, в данном случае это CombinePaths) в желаемое состояние для тестирования.Это не применимо здесь.
  • Вызывает ли DOC какое-либо недерминистское поведение (дата / время, случайность, сетевые подключения)?Дело не в этом.
  • Вызывает ли использование оригинального DOC неприемлемо длительное время сборки / выполнения?Вероятно, нет.
  • Имеют ли проблемы со стабильностью (зрелостью) DOC, которые делают тесты ненадежными, или, что еще хуже, DOC еще даже не доступен?Дело не в этом.

Если все вышеперечисленное не относится, зачем издеваться?Вам не нужно издеваться над всем догматично.Например, вы также не высмеиваете стандартные библиотечные математические функции, такие как sin или cos, потому что они также не имеют никаких вышеупомянутых проблем.

По другой теме: вы говорите «тесты»должен проверить интерфейс, а не реализацию ".Я не согласен: тесты должны найти ошибки в вашем коде.Ошибки в реализации.Различные реализации одной и той же функциональности будут иметь разные ошибки.Если реализация была не важна, зачем заботиться о покрытии кода?Конечно, наличие поддерживаемых тестов и тестов, которые не ломаются без необходимости в случае рефакторинга, являются хорошими целями (почему тестирование через публичный API, как правило, хороший подход - но не всегда), но они являются вторичными целями по сравнению с целью найти все ошибки.

0 голосов
/ 28 февраля 2019

Если использование реализации в тесте не имеет побочных эффектов, используйте их.

Если у членов, которые будут использоваться в реализации, есть побочные эффекты, тогда замените их при тестировании в изоляции, чтобы избежать нежелательного поведения.

Ложная конкретная реализация, позволяющая CallBase, чтобы он знал, что реальные членыне были переопределены

var mock = new Mock<ConcreteFileSystem>() {
    CallBase = true;
};

настройка / переопределение членов с побочными эффектами,.

mock.Setup(_ => _.ReadFile(It.IsAny<string>())
    .Returns(string.Empty); //Or what ever content you want

Обратите внимание, однако, что элементы, которые должны быть переопределены при реализации, должны быть виртуальными или абстрактными.

public class ConcreteFileSystem : IFileSystem {
    public virtual string CombinePaths(string path1, string path2) {
        return System.IO.Path.Combine(path1, path2);
    }

    public virtual string ReadFile(string path) {
        return System.IO.File.ReadAllText(path);
    }
}
...