我正试图养成用我的代码定期编写单元测试的习惯,但是我已经读过,首先要编写可测试的代码很重要。
这个问题触及编写可测试代码的SOLID原则,但我想知道这些设计原则是否有益(或至少无害),而无需计划编写测试。需要澄清的是-我了解编写测试的重要性;

为了说明我的困惑,在启发这个问题的那篇文章中,作者举了一个例子来检查当前时间,并根据时间。作者指出这是错误的代码,因为它会产生内部使用的数据(时间),因此很难进行测试。但是,对我而言,将时间作为争论似乎有点过头了。在某个时候需要初始化值,为什么不最接近消耗呢?另外,在我看来,该方法的目的是基于当前时间返回一些值,通过将其作为参数,您暗示可以/应该更改此目的。这个问题以及其他问题,使我想知道可测试的代码是否与“更好的”代码同义。

即使没有测试,编写可测试的代码仍然是一种好习惯吗?


可测试的代码实际上更稳定吗?建议重复。但是,这个问题与代码的“稳定性”有关,但是我在更广泛地询问代码是否由于其他原因(例如可读性,性能,耦合性等)是否优越。

评论

该函数有一个特殊的属性,要求您传递称为幂等的时间。每次使用给定的参数值调用此函数时,它都会产生相同的结果,这不仅使其更具可测试性,而且可组合性更高,并且更易于推理。

您可以定义“更好的代码”吗?您是指“可维护”吗?“无需使用IOC容器即可轻松使用魔术”吗?

我猜您从未有过测试失败的情况,因为它使用了实际的系统时间,然后更改了时区偏移。

它比无法测试的代码要好。

@RobertHarvey我不会称它为幂等,我会说它是参照透明性:如果func(X)返回“ Morning”,那么将所有出现的func(X)替换为“ Morning”将不会更改程序(即,调用func除了返回值外,不执行任何其他操作)。幂等性意味着func(func(X))== X(类型不正确)或func(X); func(X);与func(X)具有相同的副作用(但此处没有副作用)

#1 楼

关于单元测试的通用定义,我要说不。我已经看到简单的代码令人费解,因为需要对其进行扭曲以适合测试框架(例如,接口和IoC到处都使通过接口调用和数据层难以理解,而魔术应该很明显地传递这些层)。考虑到在易于理解的代码还是易于进行单元测试的代码之间进行选择,我每次都会选择可维护的代码。

这并不意味着不进行测试,而是要适应工具适合您,而不是相反。还有其他测试方法(但是难以理解的代码始终是错误的代码)。例如,您可以创建粒度较小的单元测试(例如,Martin Fowler认为单元通常是一个类,而不是方法),或者可以使用自动集成测试来编写程序。这样的效果可能不如您的测试框架亮起绿色的勾号,但是我们是在经过测试的代码之后,而不是过程的游戏化,对吧?

您可以使代码易于维护和维护。在单元测试之间定义良好的接口,然后编写行使组件公共接口的测试,仍然对单元测试有好处;或者您可以获得更好的测试框架(可以在运行时替换函数以对其进行模拟,而不是要求使用适当的模拟来编译代码)。更好的单元测试框架可让您在运行时用自己的系统替换系统GetCurrentTime()功能,因此您无需为此而引入人工包装就可以适合测试工具。

评论


评论不作进一步讨论;此对话已移至聊天。

–世界工程师
15年7月2日在18:44

我认为值得一提的是,我至少知道一种语言,它可以让您执行上一段所描述的内容:带Mock的Python。由于模块导入的工作方式,除了关键字之外,几乎所有其他内容都可以用模拟甚至标准API方法/类/等替换。因此这是可能的,但是可能需要以支持这种灵活性的方式来设计语言。

– jpmc26
15年7月4日在6:26



我认为“可测试代码”和“适合测试框架的代码(扭曲)”之间存在区别。我不知道该评论的内容,除非说我同意“扭曲”代码是不好的,而具有良好接口的“可测试”代码是好的。

–布莱恩·奥克利(Bryan Oakley)
15年7月5日在22:28

我在文章的评论中表达了我的一些想法(由于这里不允许扩展评论),请检查!需要明确的是:我是上述文章的作者:)

–谢尔盖·科洛迪(Sergey Kolodiy)
15年7月6日在9:16



我必须同意@BryanOakley。 “可测试的代码”表明您的关注点是分开的:可以测试方面(模块)而不会受到其他方面的干扰。我会说这不同于“调整项目支持特定的测试约定”。这类似于设计模式:不应强行使用它们。正确利用设计模式的代码将被视为强代码。同样适用于测试原理。如果使代码“可测试”导致过度扭曲项目的代码,则说明您在做错事。

–二恶英
16年6月6日19:13



#2 楼


即使在没有测试的情况下,编写可测试的代码是否仍然是一种好习惯? 。没有单元测试意味着您还没有完成代码/功能。

顺便说一句,我不会说编写可测试的代码很重要-编写灵活的代码很重要。僵化的代码很难测试,因此方法和人们所说的有很多重叠。

所以对我来说,编写代码总是有一系列优先事项:



使它工作-如果代码不起作用

使其可维护-如果代码不可维护,它将迅速停止工作。

使其具有灵活性-如果代码不灵活,当业务不可避免时,它将停止工作,并询问代码是否可以执行XYZ。

将其快速处理-超出基本可接受的水平,性能简直就是肉汁。

单元测试有助于代码的可维护性,但仅限于一点。如果使代码的可读性降低或使单元测试更脆弱,则将适得其反。 “可测试代码”通常是灵活的代码,因此很好,但不如功能或可维护性那么重要。对于当前的情况,使其具有灵活性是不错的选择,但是它会使代码更难于正确使用和更加复杂,从而损害了可维护性。由于可维护性更为重要,因此即使测试性较差,我通常也会偏向于更简单的方法。

评论


我喜欢您指出的可测试性和灵活性之间的关系,这使我更容易理解整个问题。灵活性使您的代码可以适应,但一定会使它变得更抽象,理解起来也不太直观,但这是为获得好处而付出的宝贵牺牲。

– WannabeCoder
15年7月1日在15:19

就是说,我经常看到应该被私有化的方法被强制公开或打包,以便单元测试框架能够直接访问它们。远非理想的方法。

– jwenting
2015年7月2日,下午5:37

@WannabeCoder当然,只有在最终节省您时间的情况下,才需要增加灵活性。这就是为什么我们不针对接口编写每个方法的原因-在大多数情况下,重写代码要容易得多,而不是从一开始就引入太多的灵活性。 YAGNI仍然是一个非常强大的原则-只要确保无论“不需要”是什么,回溯地添加它都不会比平均提前实施更多的工作。在我的经验中,灵活性最高的问题是不遵循YAGNI的代码。

–罗安
15年7月2日在7:45

“没有单元测试意味着您还没有完成代码/功能”-不正确。 “完成的定义”是团队决定的事情。它可能包含或不包含一定程度的测试范围。但是,没有任何地方有严格的要求,即如果没有测试,就不能“完成”功能。团队可以选择要求测试,也可以不要求。

–aroth
15年7月3日,11:22



@Telastyn在10多年的开发中,我从未有一个团队强制执行单元测试框架,只有两个团队甚至只有一个(覆盖面很差)。一个地方需要一份有关如何测试所编写功能的Word文档。而已。也许我很不幸?我不是反单元测试(严重的是,我修改了SQA.SE站点,我是非常专业的单元测试!),但是我没有发现它们像您的声明所声称的那样广泛。

– corsiKa
15年7月3日在16:54

#3 楼

是的,这是个好习惯。原因是可测试性不是为了测试。它附带的目的是为了清楚和易于理解。

没有人关心测试本身。生活中一个可悲的事实是,我们需要大型的回归测试套件,因为我们没有足够的才能编写出完美的代码,而又不会不断检查自己的立足点。如果可以的话,测试的概念将是未知的,所有这些都不是问题。我当然希望可以。但是经验表明,我们几乎所有人都做不到,因此涵盖代码的测试是一件好事,即使它们浪费了编写业务代码的时间。

测试如何独立地改善我们的业务代码测试本身?通过强迫我们将功能划分为易于证明正确的单位。这些单元比我们原本想写的单元更容易正确设置。

您的时间示例很好。只要您只有一个返回当前时间的函数,您可能会认为对其进行编程毫无意义。做到这一点有多难?但是不可避免地,您的程序将在其他代码中使用此功能,并且您肯定要在不同的条件下(包括在不同的时间)测试该代码。因此,能够操纵函数返回的时间是一个好主意-不是因为您不信任单行currentMillis()调用,而是因为您需要在受控情况下验证该调用的调用方。因此,您可以看到,即使代码本身可以测试,也很有用,但这似乎并没有引起太多关注。

评论


另一个示例是,如果您想将一个项目的代码的一部分拉到其他位置(无论出于何种原因)。功能的不同部分越相互独立,就越容易准确地提取您所需的功能,仅此而已。

–valenterry
15年7月1日在15:14

没有人会在乎测试本身,我会的。我发现测试比任何注释或自述文件都更能说明代码的功能。

– jcollum
15年7月1日在17:59

我已经慢慢阅读了一段时间的测试实践(以某种方式谁根本没有进行单元测试),我不得不说,最后一部分是在受控环境下验证调用,以及随附的更灵活的代码它使各种各样的东西都点击到位。谢谢。

– plast1k
2015年7月2日,下午3:54

#4 楼


有时需要初始化值,为什么不最接近消耗量呢?


因为您可能需要重用该代码,其值与生成的代码不同内部。插入将要用作参数的值的功能可确保您可以根据需要随时生成这些值,而不仅仅是“ now”(在调用代码时意为“ now”)。

使代码可测试实际上意味着使代码可以(从一开始)就可以在两种不同的场景(生产和测试)中使用。

基本上,您可以说没有动机在没有测试的情况下使代码可测试,编写可重用的代码有很大的优势,并且两者都是同义词。


另外,我认为该方法的目的是通过将其作为参数返回基于当前时间的一些值,这意味着可以/应该更改此目的。


您还可以辩称此方法的目的是返回一些基于时间值的值,您需要它来基于“现在”生成该值。其中一种更为灵活,如果您习惯选择该变体,那么随着时间的流逝,您的代码重用率将会提高。

#5 楼

这样说似乎很愚蠢,但是如果您希望能够测试您的代码,那么可以,编写可测试的代码会更好。您会问:


有时需要初始化该值,为什么不最接近于消费呢?


正是因为,在您引用的示例中,它使该代码不可测试。除非您仅在一天的不同时间运行部分测试。或者您重置系统时钟。或其他解决方法。所有这些都比简单地使代码具有灵活性要差。

除了不灵活之外,该小方法还具有两个职责:(1)获取系统时间,然后(2)返回一些值。基于它。

public static string GetTimeOfDay()
{
    DateTime time = DateTime.Now;
    if (time.Hour >= 0 && time.Hour < 6)
    {
        return "Night";
    }
    if (time.Hour >= 6 && time.Hour < 12)
    {
        return "Morning";
    }
    if (time.Hour >= 12 && time.Hour < 18)
    {
        return "Afternoon";
    }
    return "Evening";
}


进一步分解责任是有意义的,这样,不受控制的部分(DateTime.Now)对其余部分的影响最小。您的代码。这样做会使上面的代码更简单,并且具有可系统测试的副作用。

评论


因此,您必须在清晨进行测试,以确保在需要时获得“夜晚”的结果。那很难。现在,假设您想检查日期处理在2016年2月29日是否正确...而且,一些iOS程序员(可能还有其他)都受到初学者的错误的困扰,该错误会在今年年初前后不久将事情弄乱,您该怎么办?为此测试。根据经验,我将检查2020年2月2日的日期处理。

– gnasher729
15年7月1日在21:30

@ gnasher729正是我的意思。 “使此代码可测试”是一个简单的更改,可以解决许多(测试)问题。如果您不想自动化测试,那么我想代码可以按原样通过。但是,一旦“可测试”,那就更好了。

–埃里克·金(Eric King)
15年7月1日在21:53

#6 楼

当然,它是有成本的,但是有些开发人员习惯于付钱,以至于他们忘记了那里的成本。以您的示例为例,您现在有两个单位而不是一个,您需要调用代码来初始化和管理其他依赖项,而GetTimeOfDay更可测试,但您又回到同一条船上测试新的IDateTimeProvider。只是,如果您具有良好的测试,则收益通常会超过成本。

另外,在一定程度上,编写可测试的代码鼓励您以更可维护的方式构建代码。新的依赖关系管理代码很烦人,因此,如果可能的话,您将希望将所有与时间相关的函数组合在一起。这可以帮助减轻和修复错误,例如,当您在时间边界上正确加载页面时,使用前时间渲染某些元素,而使用后时间渲染某些元素。通过避免重复获取当前时间的系统调用,它还可以加快您的程序。

当然,这些体系结构上的改进很大程度上取决于有人注意到并实现了这些机会。过于专注于单元的最大危险之一就是忽视全局。

许多单元测试框架让您在运行时猴子模拟对象的修补,这使您获得了可测试性的好处。没有所有的混乱。我什至看到它是用C ++完成的。在看起来可测试性成本不值得的情况下研究该功能。

评论


+1-您确实需要改进设计和体系结构以简化编写单元测试。

–BЈовић
15年7月1日在16:21

+-重要的是您的代码架构。更轻松的测试只是一个令人愉快的副作用。

– gbjbaanb
15年7月2日在9:21

#7 楼

在可测试性之外,并非所有有助于可测试性的特性都是可取的-例如,我很难为您引用的时间参数提出与测试无关的理由-但从广义上讲,有助于可测试性的特性无论可测试性如何,它也有助于编写出良好的代码。

广义上讲,可测试代码是可延展代码。它以小块,离散块,内聚块形式存在,因此可以调用各个位以进行重用。它的组织和名称都很好(为了能够测试某些功能,您应该更加注意命名;如果您不编写测试,那么一次性函数的名称就没那么重要了)。它往往更具参数性(例如您的时间示例),因此可以从其他上下文中使用,而不是最初的预期目的。它很干燥,所以很整洁,更容易理解。

是的。最好编写可测试的代码,甚至不考虑测试。

评论


不同意它是干的-将GetCurrentTime封装在方法中MyGetCurrentTime非常重复OS调用,除了协助测试工具外没有任何好处。那只是最简单的例子,实际上它们变得更糟。

– gbjbaanb
15年7月2日在9:18

“无益地拒绝OS调用” –直到您最终在一个时钟的服务器上运行,在不同时区与aws服务器通话,这破坏了代码,然后您必须遍历所有代码并更新它以使用MyGetCurrentTime,它返回UTC。 ;时钟偏斜,夏令时,以及其他原因导致盲目信任OS调用可能不是一个好主意,或者至少有一个地方可以加入另一个替代品。

–安德鲁·希尔(Andrew Hill)
15年7月4日在10:06

#8 楼

如果您想证明自己的代码确实可以工作,那么编写可测试的代码就很重要。

我倾向于同意将代码扭曲为令人讨厌的扭曲,以使其适合特定测试的负面观点。另一方面,这里的每个人都不得不处理1000行长的魔术函数,这是必须要处理的,实际上是不可能的在不破坏一个或多个其他地方(或自身内部某个地方几乎不可能可视化的依赖)的模糊不明显的依赖关系的情况下进行触摸,并且从定义上讲几乎是不可测试的。我认为,测试框架变得过分夸大的观点(并非没有优点)不应被视为免费的许可,可以编写质量低,不可测试的代码。

测试驱动的开发理想确实倾向于例如,促使您编写单一职责的程序,那绝对是一件好事。就我个人而言,我说要买入单一责任,单一事实来源,可控制范围(没有freakin的全局变量),并将脆弱的依存关系降至最低,您的代码将是可测试的。是否可以通过某些特定的测试框架进行测试?谁知道。但是也许是测试框架需要调整自身以适应良好的代码,而不是反过来。

但是要清楚一点,代码是如此聪明,或者如此之长,并且/或者相互依赖。不容易被另一个人理解的代码不是好代码。巧合的是,它也不是可以轻松测试的代码。

所以接近我的总结,可测试的代码是否是更好的代码?

我不知道,也许不。这里的人有一些有效的观点。

但是我确实相信更好的代码也倾向于是可测试的代码。

而且,如果您正在谈论的是用于认真工作的严肃软件,那么交付未经测试的代码并不是您用雇主或客户的钱所能做的最负责任的事情。

这也是事实某些代码比其他代码需要更严格的测试,并且假装否则有点愚蠢。如果未测试将您与航天飞机上的重要系统连接的菜单系统,您希望成为航天飞机上的宇航员吗?还是没有对用于监测反应堆温度的软件系统进行测试的核电站的员工?另一方面,生成一个简单的只读报告的一些代码是否需要一个装满文档和一千个单元测试的容器卡车?我当然希望不会。只是说...

评论


“更好的代码也往往是可测试的代码”,这是关键。使它可测试并不能使其更好。使其变得更好通常可以使其可测试,并且测试通常会为您提供可以用来使其更好的信息,但是仅存在测试并不意味着质量,并且(很少)例外。

–anaximander
15年7月2日在10:30

究竟。考虑相反的。如果它是不可测试的代码,则未经测试。如果未经测试,除了现场情况外,您如何知道它是否有效?

– pjc50
15年7月2日在10:39

所有测试都证明代码已通过测试。否则,经过单元测试的代码将没有错误,并且我们知道情况并非如此。

– wobbily_col
15年7月2日在11:26

@anaximander确实如此。如果所有的重点只是检查复选框,那么至少存在测试的禁忌症可能会导致代码质量较差,这至少是有可能的。 “每个功能至少要进行七个单元测试?” “检查。”但是我确实相信,如果代码是高质量的代码,它将更易于测试。

– Craig
15年7月2日在20:51

...但是使官僚机构退出测试可能是完全浪费,并且不会产生有用的信息或可信赖的结果。而不管;我当然希望有人测试过SSL Heartbleed错误,是吗?还是Apple Goto失败错误?

– Craig
15年7月2日在21:06



#9 楼


但是,对我来说,将时间作为参数传递似乎太过分了。


您是对的,通过模拟可以使代码可测试并避免消磨时间(不确定的双关意图)。示例代码:

def time_of_day():
    return datetime.datetime.utcnow().strftime('%H:%M:%S')


现在假设您要测试a秒发生的情况。如您所说,要测试这种过大的方式,您必须更改(生产)代码:

def time_of_day(now=None):
    now = now if now is not None else datetime.datetime.utcnow()
    return now.strftime('%H:%M:%S')


如果Python支持leap秒,则测试代码应如下所示: :

def test_handle_leap_second(self):
    actual = time_of_day(
        now=datetime.datetime(year=2015, month=6, day=30, hour=23, minute=59, second=60)
    expected = '23:59:60'
    self.assertEquals(actual, expected)


您可以进行测试,但是代码比必要的更为复杂,并且测试仍然无法可靠地执行大多数生产代码将使用的代码分支(即,不传递now的值)。您可以通过使用模拟解决此问题。从原始生产代码开始:

def time_of_day():
    return datetime.datetime.utcnow().strftime('%H:%M:%S')


测试代码:

@unittest.patch('datetime.datetime.utcnow')
def test_handle_leap_second(self, utcnow_mock):
    utcnow_mock.return_value = datetime.datetime(
        year=2015, month=6, day=30, hour=23, minute=59, second=60)
    actual = time_of_day()
    expected = '23:59:60'
    self.assertEquals(actual, expected)


这具有以下几个优点:


要独立于time_of_day进行测试。
要测试与生产代码相同的代码路径。
生产代码尽可能简单。

顺便提一下,希望将来的模拟框架将使这种事情变得更容易。例如,由于必须将模拟函数引用为字符串,因此当time_of_day开始使用其他时间来源时,很难轻易使IDE自动更改它。

评论


仅供参考:您的默认参数错误。它只会被定义一次,因此您的函数将始终返回第一次被评估的时间。

– ahruss
15年7月4日在17:41

#10 楼

编写良好的代码的质量在于更改的鲁棒性。也就是说,当需求发生变化时,代码中的变化应成比例。这是一种理想的方法(但并非总是可以实现),但是编写可测试的代码有助于使我们更接近这个目标。

为什么它有助于使我们更紧密?在生产中,我们的代码在生产环境中运行,包括与我们所有其他代码的集成和交互。在单元测试中,我们清除了大部分这种环境。现在,我们的代码具有很强的更改能力,因为测试是更改。与生产中使用的单元不同,我们以不同的方式使用这些单元,它们具有不同的输入(模拟,错误的输入,这些输入可能永远都不会传入)。

这将为当天的代码做准备当我们的系统发生变化时。假设我们的时间计算需要根据时区花费不同的时间。现在,我们可以传递时间,而不必对代码进行任何更改。当我们不想传递时间并想使用当前时间时,我们可以使用默认参数。我们的代码可更改,因此健壮,因为它是可测试的。

#11 楼

根据我的经验,在构建程序时做出的最重要,最深远的决定之一就是如何将代码分解为多个单元(其中“ units”是最广义的用法)。如果使用的是基于类的OO语言,则需要将用于实现程序的所有内部机制分解为一些类。然后,您需要将每个类的代码分解为一些方法。在某些语言中,选择是如何将代码分解为功能。或者,如果您做SOA,则需要确定要构建多少服务以及将要包含在每个服务中的内容。

选择的故障对整个过程有很大的影响。好的选择可以使代码更易于编写,并减少错误(甚至在开始测试和调试之前)。它们使更改和维护变得更加容易。有趣的是,事实证明,一旦找到了良好的故障,通常也比较差的故障更容易测试。

为什么会这样呢?我认为我无法理解和解释所有原因。但是一个原因是,良好的故障分析总是意味着要为实施单位选择适当的“粒度”。您不想将太多的功能和太多的逻辑塞入单个类/方法/功能/模块/等中。这使您的代码更易于阅读和编写,但也使测试更加容易。

不仅如此。良好的内部设计意味着可以清楚,准确地定义每个实现单元的预期行为(输入/输出/等)。这对于测试很重要。好的设计通常意味着每个实现单元对其他单元都有一定程度的依赖性。这使您的代码更易于他人阅读和理解,也使测试更加容易。原因还在继续;也许其他人可以阐明我无法理解的更多原因。
对于您问题中的示例,我认为“良好的代码设计”并不等同于说所有外部依赖项(例如对系统时钟的依赖项)都应始终“注入”。这可能是个好主意,但这是与我在此处描述的问题不同的问题,我不会深入研究其优缺点。

即使您直接调用系统函数,也不会它返回当前时间,对文件系统进行操作,依此类推,这并不意味着您无法单独对代码进行单元测试。诀窍是使用标准库的特殊版本,该版本允许您伪造系统函数的返回值。我从未见过其他人提到这种技术,但是使用许多语言和开发平台都非常简单。 (希望您的语言运行时是开源的,并且易于构建。如果执行代码涉及链接步骤,希望它也很容易控制链接到的库。)

总而言之,可测试代码不一定是“好”代码,但“好”代码通常是可测试的。

#12 楼

如果您使用SOLID原理,那么您将处于有利的一面,尤其是在使用KISS,DRY和YAGNI进行扩展时。

对我来说,一个缺失点是方法的复杂性。这是一个简单的getter / setter方法吗?然后仅仅编写测试来满足您的测试框架将是浪费时间。

如果这是一种更复杂的方法,您可以在其中操纵数据并希望确保即使更改了它也可以正常工作。内部逻辑,那么对测试方法将是一个很好的呼吁。很多次,我不得不在几天/几周/几月后更改一段代码,我真的很高兴拥有测试用例。最初开发该方法时,我用测试方法对其进行了测试,并且确信它会起作用。更改后,我的主要测试代码仍然有效。因此,我确定所做的更改不会破坏生产中的某些旧代码。

编写测试的另一方面是向其他开发人员展示如何使用您的方法。开发人员常常会搜索有关如何使用方法以及返回值将是什么的示例。

我只有两美分。