关于单元测试:如何测试/更改未测试和不可测试的代码?

关于单元测试:如何测试/更改未测试和不可测试的代码?

How do you test/change untested and untestable code?

最近,我不得不在并非所有代码都具有单元测试的旧系统上更改一些代码。
在进行更改之前,我想编写测试,但是每个类都创建了很多依赖关系和其他反模式,这使得测试非常困难。
显然,我想重构代码以使其更易于测试,编写测试然后进行更改。
这是您的方式吗? 还是您会花费大量时间来编写难以编写的测试,这些测试在重构完成后会被大部分删除?


首先,这是一篇很棒的文章,其中包含有关单元测试的提示。其次,我发现一种避免对旧代码进行大量更改的好方法是将其重构一点,直到可以对其进行测试。一种简单的方法是使私有成员受保护,然后覆盖受保护的字段。

例如,假设您有一个在构造函数期间从数据库加载一些内容的类。在这种情况下,您不仅可以覆盖受保护的方法,还可以将数据库逻辑提取到受保护的字段,然后在测试中覆盖它。

1
2
3
4
5
public class MyClass {
    public MyClass() {
        // undesirable DB logic
    }
}

变成

1
2
3
4
5
6
7
8
9
public class MyClass {
    public MyClass() {
        loadFromDB();
    }

    protected void loadFromDB() {
        // undesirable DB logic
    }
}

然后您的测试如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyClassTest {
    public void testSomething() {
        MyClass myClass = new MyClassWrapper();
        // test it
    }

    private static class MyClassWrapper extends MyClass {
        @Override
        protected void loadFromDB() {
            // some mock logic
        }
    }
}

这有点不好,因为您可以在这种情况下使用DBUnit,但是最近我实际上在类似的情况下使用了DBUnit,因为我想测试一些与加载的数据完全无关的功能,因此非常有效。我还发现,这样的成员公开在其他类似的情况下很有用,在这些情况下,我需要摆脱很长时间以来一直存在于类中的某些依赖项。

但是,如果您正在编写框架,我建议不要使用此解决方案,除非您真的不介意将成员公开给框架的用户。

这有点骇人听闻,但我发现它非常有用。


@valters

我不同意您的说法,即测试不应破坏构建。测试应表明该应用程序未引入针对测试功能的新错误(发现的错误表明缺少测试)。

如果测试没有破坏构建,那么您很容易遇到新代码破坏构建且即使测试覆盖了一段时间也无法得知的情况。测试失败应该是一个红色标记,表明必须修复测试或代码。

此外,允许测试不中断构建会导致故障率缓慢上升,以至于您不再拥有可靠的一组回归测试。

如果测试频繁中断存在问题,则可能表明测试以过于脆弱的方式编写(取决于可能更改的资源,例如未正确使用DB Unit的数据库或外部Web服务(应该被嘲笑),否则可能表明团队中有一些开发人员没有对测试给予应有的重视。

我坚信失败的测试应该尽快修复,就像修复无法编译的代码一样。


我已经阅读了有效使用遗留代码,并且我同意它对于处理"令人困惑的"代码非常有用。

有些技术仅适用于编译语言(我正在使用"旧" PHP应用程序),但是我想说本书的大部分内容都适用于任何语言。

重构书籍有时会在重构之前假定代码处于半理想状态或"维护意识"状态,但是我使用的系统并不理想,而是作为"边学边学"的应用程序开发的,或者是某些使用的技术的第一个应用程序(而且我不怪最初的开发人员,因为我是其中的一员),因此根本没有测试,并且代码有时很混乱。这本书解决了这种情况,而其他重构书籍通常没有(嗯,就此而言)。

我应该提及的是,我没有从本书的编辑和作者那里得到任何款项;),但我发现它非常有趣,因为在遗留代码领域(尤其是在我的语言法语中)缺乏资源,但这就是另一个故事)。


我不确定为什么您会说重构完成后就将删除单元测试。实际上,您的单元测试套件应在主构建之后运行(您可以创建一个单独的"测试"构建,该构建仅在主产品构建之后运行单元测试)。然后,您将立即看到一件产品中的更改是否破坏了其他子系统中的测试。请注意,这与在构建过程中运行测试有些不同(某些人可能会主张)-某些有限的测试在构建过程中很有用,但通常由于某些单元测试碰巧失败而"崩溃"构建毫无用处。

如果您正在编写Java(有可能),请访问http://www.easymock.org/-对于减少测试的耦合可能很有用。


推荐阅读