
通过代码测试,一切都变得清楚了(嗯,至少是需要编写它们的事实)。 通过配置测试,从它们的存在开始,一切都不那么明显。 有人写吗? 重要吗? 难吗 他们的帮助可以取得什么样的结果?
事实证明,这也是非常有用的,开始做起来非常简单,同时在测试配置时有很多细微差别。 哪一个-根据实际经验画画。
该材料基于Ruslan cheremin Cheremin (德意志银行的Java开发人员)的报告的笔录 。 接下来是第一人称演讲。我叫Ruslan,我在德意志银行工作。 我们从这个开始:

文本很多,从远处看来似乎是俄语。 但这不是事实。 这是一种非常古老和危险的语言。 我翻译成简单的俄语:
我将简要描述我今天要谈论的内容。 假设我们有一个代码:

也就是说,最初我们有某种任务,我们编写了代码来解决它,据说它能为我们赚钱。 如果由于某种原因该代码无法正常工作,则说明解决了错误的任务并为我们赚了钱。 企业不喜欢这种钱-它们在财务报表上看起来很糟糕。
因此,对于我们的重要代码,我们进行了测试:

通常在那里。 现在,大概每个人都拥有它。 测试验证该代码能够解决正确的问题并赚到正确的钱。 但是服务不仅限于代码,在代码旁边还有一个配置:

至少在我参与的几乎所有项目中,这种配置都是一种形式。 (在我早期的UI时代,我只能回忆起几个案例,当时没有配置文件,但所有内容都是通过UI进行配置的)。在此配置中,有端口,地址和算法参数。
为什么配置对测试很重要?
这是窍门:配置中的错误对程序执行的危害不小于代码中的错误。 它们也可能导致代码执行错误的任务-参见上文。
由于在配置中通常无法编译,因此在配置中查找错误比在代码中更难。 我以属性文件为例,通常有不同的选项(JSON,XML,有人在YAML中存储),但是重要的是,不要编译任何内容,因此不要对其进行检查。 如果您不小心将Java文件密封了-最有可能的是,它根本不会通过编译。 财产上的随机错字不会激发任何人,它将起作用。
而且IDE也不会突出显示配置中的错误,因为它仅了解关于属性文件格式的最原始的信息:例如,应该有一个键和一个值,以及在它们之间应该有一个“等于”,冒号或空格。 但是实际上值必须是数字,网络端口或地址-IDE不知道任何内容。
即使您在UAT或暂存环境中测试应用程序,也不能保证任何事情。 通常,由于每个环境中的配置都不同,因此在UAT中,您仅测试了UAT配置。
另一个微妙之处是,即使在生产中,有时也不会立即出现配置错误。 服务可能根本无法启动-这是一个好方案。 但是它可以启动并工作很长时间-直到X时刻为止,此时将有必要精确地确定错误所在的参数。 在这里,您发现最近没有改变的服务突然停止工作。
毕竟,我说过-测试配置似乎应该是一个热门话题。 但实际上,它看起来像这样:

至少在某些情况下,我们的情况如此。 我的报告的任务之一就是也不要让您看起来像这样。 希望我能够将您推向这一点。
三年前,在我的团队中,安德烈·萨塔林(Andrei Satarin)在我们的德意志银行担任质量保证负责人。 是他带来了测试配置的想法-也就是说,他只是参加了并进行了第一个这样的测试。 六个月前,在之前的Heisenbug中,他发表了关于测试配置的
演讲 。 我建议您看一下,因为他在那里对问题进行了广泛的看:从科学文章的角度以及从遇到配置错误及其后果的大公司的经验中都可以看出。
我的报告将更窄-有关实际经验。 我将讨论编写配置测试时作为开发人员遇到的哪些问题,以及如何解决这些问题。 我的决定可能不是最佳决定,这些也不是最佳实践-这是我的个人经验,我尝试不做广泛的概括。
报告概述:
- “周一下午之前您可以做什么”:简单,有用的示例。
- “两年后的星期一”:在哪里以及如何做得更好。
- 支持重构配置:如何实现密集覆盖; 软件配置模型。
第一部分是激励性的:我将描述一切从我们开始的最简单的测试。 将会有各种各样的例子。 我希望其中至少有一个能引起您的共鸣,即您会看到某种类似的问题及其解决方案。
第一部分的测试本身是简单的,甚至是原始的-从工程学的角度来看,没有火箭科学。 但是,只要它们可以很快完成就特别有价值。 这是“轻松进入”配置测试的过程,并且非常重要,因为编写这些测试存在心理障碍。 我想表明“您可以做到”:现在,我们做到了,效果很好,虽然没有人死亡,但我们已经生活了三年。
第二部分是有关之后的操作。 当您编写许多简单的测试时,就会出现支持问题。 它们中的一些开始掉落,您了解它们应该突出显示的错误。 事实证明,这并不总是很方便。 问题是编写更复杂的测试-毕竟,您已经介绍了简单的案例,我想要更有趣的东西。 同样,这里也没有最佳实践,我只介绍一些对我们有用的解决方案。
第三部分是关于测试如何支持重构相当复杂和令人困惑的配置的。 再次进行案例研究-我们如何做到的。 从我的角度来看,这是一个示例,说明如何扩展配置测试以解决更大的任务,而不仅仅是塞小孔。
第1部分:“您可以那样做”
现在,很难理解我们进行的首次配置测试是什么。 安德烈坐在大厅里,他可以说我撒了谎。 但是在我看来,这一切都始于此:

情况是这样的:我们在同一主机上有n个服务,每个服务都在其端口上引发自己的JMX服务器,并导出一些监视JMX。 在文件中配置了所有服务的端口。 但是该文件占用多个页面,并且还有许多其他属性-经常会导致不同服务的端口发生冲突。 容易犯错误。 然后,一切都是微不足道的:某些服务不会增加,在此之后对于依赖它的人来说它们不会增加-测试人员很生气。
这个问题分几行解决。 这个测试(在我看来)是我们的第一个测试,看起来像这样:

没什么复杂的:我们浏览配置文件所在的文件夹,加载它们,将它们解析为属性,过滤出名称包含“ jmx.port”的值,并检查所有值是否唯一。 甚至不需要将值转换为整数。 大概只有端口。
当我看到这种情况时,我的第一反应是:

第一印象:在我漂亮的单元测试中这是什么? 为什么我们要进入文件系统?
然后惊喜来了:“那是什么?”
我说这是因为似乎存在某种心理障碍,因此很难编写此类测试。 从那时起已经过去了三年,这个项目充满了这样的测试,但是我经常看到我的同事碰到配置错误,不编写任何测试。 对于该代码,每个人都已经习惯于编写回归测试-因此不再重现发现的错误。 但是他们没有进行配置,这会干扰某些事情。 有某种心理上的障碍需要解决-这就是为什么我提到这样的反应,以便您可以从中识别出它。

以下示例几乎相同,但略有修改-我删除了所有“ jmx”。 这次我们检查所有称为“端口”的属性。 它们必须是整数值,并且是有效的网络端口。 Matcher validNetworkPort()隐藏了我们自定义的Hamcrest Matcher,它检查该值是否高于系统端口范围,低于临时端口范围,好吧,我们知道服务器上的某些端口已被占用-这是它们的全部列表隐藏在这是火柴人。
这个测试还是很原始的。 请注意,其中没有任何迹象表明我们正在检查哪个特定属性-它很大。 一个这样的测试可以检查名称为“ ... port”的500个属性,并在所有必要条件下验证所有这些属性都是期望范围内的整数。 他们写完之后,就打了十几行。 这是一个非常方便的功能,它的出现是因为配置具有简单的格式:两列,一个键和一个值。 因此,可以对其进行批量处理。
另一个测试示例。 我们在这里检查什么?

他检查真实密码是否不会泄漏到生产环境中。 所有密码应如下所示:

您可以为属性文件编写很多测试。 我不会给出更多示例-我不想重复自己的想法,这个想法很简单,那么一切都应该清楚。
...并且在编写了足够多的测试之后,出现一个有趣的问题:我们所说的配置是什么意思,它的边界在哪里? 我们将属性文件视为配置,我们对其进行了介绍-相同样式还可以涵盖哪些内容?
考虑什么配置
事实证明,项目中有许多文本文件尚未编译-至少在正常的构建过程中没有编译。 在服务器上执行它们之前,不会以任何方式对其进行验证,也就是说,它们中的错误会延迟出现。 所有这些文件(有些扩展)都可以称为配置。 至少,它们将被测试大致相同。
例如,我们有一个SQL修补程序系统,这些系统在部署过程中会滚动到数据库上。

它们是为SQL * Plus编写的。 SQL * Plus是60年代的工具,它需要各种奇怪的东西:例如,确保文件末尾在新行中。 当然,人们通常会忘记将行列结尾放在那儿,因为他们不是60年代出生的。

再用相同的十几行代码解决:选择所有的SQL文件,检查最后是否有斜杠。 简单,方便,快捷。
“像文本文件”的另一个示例是crontabs。 我们的crontab服务启动和停止。 它们最常引起两个错误:

首先,时间表表达格式。 它并不是那么复杂,但是没有人在启动前检查它,因此很容易放置一个额外的空间,逗号等。
其次,如上例所示,文件的末尾也必须在新行上。
所有这些都很容易验证。 该文件的末尾是可以理解的,但是要检查日程安排,可以找到用于解析cron表达式的现成库。 在报告之前,我用Google搜索:至少有六个。 我发现了六个,但总的来说可能还会更多。 在编写时,我们采用了其中最简单的一种,因为我们不需要检查表达式的内容,而只是检查语法的正确性,因此cron成功加载了它。
原则上,您可以结清更多支票-检查是否在一周的正确日期开始,以及在工作日中途不停止服务。 但这对我们没有太大帮助,我们也没有打扰。
另一个行之有效的想法是shell脚本。 当然,用Java编写全面的bash脚本解析器对于勇敢的人来说是一种乐趣。 但最重要的是,这些脚本中有很多都不是完整的bash。 是的,在某些bash脚本中,代码是直接的,地狱又是地狱,它们每年掉入一次,并且发誓会跑掉。 但是许多bash脚本是相同的配置。 有许多系统变量和环境变量被设置为所需的值,从而配置使用这些变量的其他脚本。 这些变量很容易从该bash文件进行grep并检查有关它们的内容。

例如,检查是否在每个环境中都安装了JAVA_HOME,或者在LD_LIBRARY_PATH中使用了一些jni库。 我们以某种方式从一种Java版本迁移到另一种Java版本,并扩展了测试:我们检查JAVA_HOME在该环境的那个子集上是否包含“ 1.8”,然后逐步将其转移到新版本。
这里有一些例子。 让我总结一下结论的第一部分:
- 起初,配置测试令人困惑,存在心理障碍。 但是,克服它之后,应用程序中有很多地方没有被检查覆盖并且可以覆盖。
- 然后可以轻松愉快地编写它们:有很多“低落的果实”可以迅速带来巨大的收益)。
- 降低检测和纠正配置错误的成本 。 由于实际上这些是单元测试,因此即使在提交之前也可以在计算机上运行它们-这大大减少了反馈循环。 例如,其中许多当然已经在测试部署阶段进行了测试。 如果这是生产配置,则许多产品将不会经过测试。 因此,可以直接在本地计算机上检查它们。
- 他们给了第二个青春。 从某种意义上说,您仍然可以测试很多有趣的东西。 确实,在代码中找到要测试的东西不再那么容易。
第2部分。更复杂的情况
让我们继续进行更复杂的测试。 在涵盖了大部分琐碎的检查(如此处所示)之后,出现了问题:是否可以检查更复杂的东西?
“更难”是什么意思? 我刚刚描述的测试大致具有以下结构:

他们对照一个特定文件检查某些内容。 也就是说,我们遍历文件,对每个文件进行一定的条件检查。 因此,可以进行很多验证,但是还有更多有用的方案:
- UI应用程序连接到其环境的服务器。
- 同一环境中的所有服务都连接到同一管理服务器。
- 同一环境中的所有服务都使用同一数据库。
例如,UI应用程序连接到其环境服务器。 UI和服务器很可能是不同的模块,如果根本不是项目,则它们具有不同的配置,因此它们不太可能使用相同的配置文件。 因此,您将必须链接它们,以便将一个环境的所有服务连接到一台密钥管理服务器,通过该服务器分发命令。 同样,很可能这些是不同的模块,不同的服务,并且通常由不同的团队来开发它们。
或所有服务都使用相同的数据库,相同的事物-不同模块中的服务。
实际上,情况是这样的:许多服务,每个服务都有其自己的配置结构,您需要减少其中的一些并在交叉点检查某些内容:

当然,您可以精确地做到这一点:加载第二个,将某物拉出某个地方,然后将其粘贴在测试代码中。 但是您可以想象代码将有多大,可读性将如何。 我们从此开始,但后来我们意识到这是多么困难。 如何做得更好?
如果您梦想着,它会更方便,那么我梦想着该测试看起来像是用人类语言解释的:
@Theory public void eachEnvironmentIsXXX( Environment environment ) { for( Server server : environment.servers() ) { for( Service service : server.services() ) { Properties config = buildConfigFor( environment, server, service );
对于每个环境,都满足一个条件。 为此,您需要从环境中查找服务器列表,服务列表。 然后加载配置并在交叉口检查一些东西。 因此,我需要这样的东西,我称它为Deployment Layout。

我们需要代码中的机会来访问应用程序的部署方式:将哪些服务放置在哪些服务器上,在哪个环境中放置-以获取此数据结构。 然后从它开始,我开始加载配置并处理它。
部署布局特定于每个团队和每个项目。 我已经画过了-这是一个一般情况:通常有一组服务器,服务,一个服务有时具有一组配置文件,而不仅仅是一组。 有时需要其他对测试有用的参数,必须添加它们。 例如,服务器所在的机架可能很重要。 Andrey在他的报告中举了一个示例,说明备份/主服务必须位于不同的机架中对于它们的服务很重要-对于他的情况,他需要在部署布局中保留对机架的引用:

就我们的目的而言,服务器区域在原则上也很重要,即特定的数据中心,因此备份/主数据库位于不同的数据中心。 这些都是所有其他服务器属性,它们是特定于项目的,但是在幻灯片上,它是一个共同点。
在哪里获得部署布局? 似乎在任何一家大公司中都有一个基础架构管理系统,所有内容都在此进行了描述,它是可靠,可靠的,而实际上……不是。
至少,我在两个项目中的实践表明,首先进行硬编码比较容易,然后在三年后……硬着头皮。
我们已经在这个项目上开展了三年了。 在第二年,我们似乎仍然与基础架构管理集成了一年,但这些年来我们一直过着这样的生活。 从经验来看,推迟与IM集成的任务以尽快获得现成的测试是有意义的,这将表明它们有效且有用。 然后可能会发现这种集成可能不是必需的,因为跨服务器的服务分配不会经常更改。
硬编码实际上可以是这样的:
public enum Environment { PROD( PROD_UK_PRIMARY, PROD_UK_BACKUP, PROD_US_PRIMARY, PROD_US_BACKUP, PROD_SG_PRIMARY, PROD_SG_BACKUP ) … public Server[] servers() {…} } public enum Server { PROD_UK_PRIMARY(“rflx-ldn-1"), PROD_UK_BACKUP("rflx-ldn-2"), PROD_US_PRIMARY(“rflx-nyc-1"), PROD_US_BACKUP("rflx-nyc-2"), PROD_SG_PRIMARY(“rflx-sng-1"), PROD_SG_BACKUP("rflx-sng-2"), public Service[] services() {…} }
我们在第一个项目中使用的最简单方法是使用每个服务器中的服务器列表来枚举Environment。 有一个服务器列表,似乎应该有一个服务列表,但是我们欺骗了:我们有启动脚本(也是配置的一部分)。

他们为每个环境运行服务。 并且services()方法只是从其服务器文件中复制所有服务。 这样做是因为环境不多,而且服务器也很少添加或删除-但是有很多服务,而且它们经常被洗牌。 从脚本加载服务的实际布局是有意义的,以免过于频繁地更改硬编码的布局。
创建了这样的软件配置模型后,将出现令人愉悦的奖励。 例如,您可以编写如下测试:

测试是在每个环境上都存在所有关键服务。 假设有四个关键服务,其余的可能有也可能没有,但是没有这四个就没有意义。 您可以确认没有忘记它们,它们都在同一环境中具有备份。 通常,在配置这些实例的UAT时会发生此类错误,但也可能泄漏到PROD中。 最后,UAT中的错误还会浪费时间和测试人员的神经。
出现了维持配置模型的相关性的问题。 您也可以为此编写测试。
public class HardCodedLayoutConsistencyTest { @Theory eachHardCodedEnvironmentHasConfigFiles(Environment env){ … } @Theory eachConfigFileHasHardCodedEnvironment(File configFile){ … } }
有配置文件,并且代码中有部署布局。 并且您可以针对每个环境/服务器/等等进行验证。 有一个相应的配置文件,并且对于每种格式的文件-相应的环境。 一旦您忘记将某物添加到一个位置,测试就会失败。
最重要的是部署布局:
- 简化了编写复杂的测试的过程,这些测试将来自应用程序不同部分的配置整合在一起。
- 使它们更清晰易读。 它们看起来像是您对它们的高级思考,而不是它们通过配置的方式。
- 在创建过程中,当人们提出问题时,事实证明,有关部署有很多有趣的事情。 例如,关于在一台服务器上托管两个环境的可能性的限制,即隐含的神圣知识。 事实证明,开发人员的想法有所不同,因此相应地编写了他们的服务。 这样的时刻对于在开发人员之间进行解决很有用。
- 很好地补充了文档(尤其是如果没有的话)。 即使有,作为开发人员,我也很高兴在代码中看到这一点。 此外,您可以在这里写下对我重要的评论,而不是其他人的评论。 您还可以硬编码。 也就是说,如果您确定同一台服务器上不能有两个环境,则可以插入支票,而现在不会。 至少您会发现有人尝试过。 也就是说,这是具有强制执行能力的文档。 这非常有帮助。
让我们继续前进。 编写测试后,它们“停顿”了一年,有些开始下降。 有些跌落开始较早,但并不那么可怕。 当一年前编写的测试跌落,看到错误消息而又听不懂时,这很可怕。

假设我理解并同意这是无效的网络端口-但这在哪里? 在讨论之前,我查看了一个事实,即项目中有1200个属性文件,分布在90个模块中,其中共有24,000行。 (尽管我很惊讶,但是如果您算的话,这不是一个很大的数目-一个服务包含4个文件。)此端口在哪里?
显然,assertThat()有一个message参数,您可以在其中输入有助于确定位置的内容。 但是,当您编写测试时,您不会考虑它。 即使您认为,您仍然必须猜测哪种描述将足够详细,以至于一年后可以理解。 我想使这一刻自动化,以便有一种方法可以自动生成或多或少清晰的描述来编写测试,从而发现错误。
再一次,我梦到并且梦想着这样的事情:
SELECT environment, server, component, configLocation, propertyName, propertyValue FROM configuration(environment, server, component) WHERE propertyName like “%.port%” and propertyValue is not validNetworkPort()
这样的伪SQL-好吧,我只知道SQL,但大脑却将解决方案从熟悉的范围中剔除了。 这个想法是,大多数配置测试都由相同类型的几部分组成。 首先,根据条件选择参数的子集:

然后,关于这个子集,我们检查关于值的一些事情:

然后,如果某些属性的值不满足要求,这就是我们希望在错误消息中收到的“工作表”:

一次,我什至曾想过是否可以编写类似于SQL的解析器,因为现在这并不困难。 但是后来我意识到IDE将不支持它并提出建议,因此人们将不得不在没有IDE提示,没有编译,没有检查的情况下盲目编写这个自制的“ SQL”,这不是很方便。 因此,我必须寻找我们的编程语言支持的解决方案。 如果我们有.NET,则LINQ会有所帮助,它几乎类似于SQL。
Java中没有LINQ,流尽可能接近。 这是测试在流中的外观:
ValueWithContext[] incorrectPorts = flattenedProperties( environment ) .filter( propertyNameContains( ".port" ) ) .filter( !isInteger( propertyValue ) || !isValidNetworkPort( propertyValue ) ) .toArray(); assertThat( incorrectPorts, emptyArray() );
flattenedProperties()接受此环境的所有配置,所有服务器,服务的所有文件,并将它们扩展到大表。 这本质上是一个类似于SQL的表,但采用一组Java对象的形式。 flattenedProperties()将这组字符串作为流返回。

然后,在这组Java对象上添加一些条件。 在此示例中:我们选择在propertyName中包含“端口”的那些,并过滤那些未将值转换为Integer或不在有效范围内的值。 这些是错误的值,从理论上讲,它们应该为空集。

如果它们不是空集,则将引发如下错误:

第3部分。测试作为重构的支持
通常,代码测试是最强大的重构支持之一。 重构是一个危险的过程,需要大量重做,我想确保在此之后该应用程序仍然可行。 确保这一点的一种方法是,首先在各个方面覆盖所有内容,然后进行重构。
现在,摆在我面前的是重构配置的任务。 有一个七年前写的应用程序。 该应用程序的配置如下所示:

这是一个例子,还有更多。 三重嵌套排列,这在整个配置中都使用:

配置本身中的文件很少,但彼此包含在一起。 它使用iu属性的一个小扩展-Apache Commons Configuration,它仅支持大括号中的包含和权限。
仅使用这两件事,作者就做了出色的工作。 我认为他在那上面建造了图灵机。 在某些地方,似乎他确实确实在尝试使用包含和替代进行计算。 我不知道这个图灵系统是否完整,但是我认为他试图证明这一点。
那个人走了。 写了,应用程序工作了,他离开了银行。 一切正常,只有没人完全了解配置。
如果我们提供一项单独的服务,那么结果将得出10个包含项,达到三层深度;如果扩展了所有内容,则总共可以包含450个参数。 实际上,此特定服务使用了其中的10%到15%,其余参数用于其他服务,因为文件是共享的,所以它们被多个服务使用。 但是究竟有10-15%的人使用此特定服务并不容易理解。 作者显然理解。 非常聪明的人,非常
任务分别是简化配置和重构。 同时,我想保持应用程序正常运行,因为在这种情况下,这种可能性很小。 我要:
- 简化配置。
- 这样,在重构之后,每个服务仍然具有其所有必需的参数。
- 这样他就没有多余的参数。 与此无关的85%的页面不应混乱。
- 该服务仍成功连接到集群并进行了协作。
问题在于,由于系统具有高度冗余性,因此目前尚不清楚它们之间的连接状况。 例如,向前看:在重构期间,事实证明,在一种生产配置中,备份剪辑中应该有四台服务器,但是实际上有两台服务器。 由于冗余级别很高,没有人注意到这一点-错误意外地浮出水面,但实际上,冗余级别很长时间一直低于我们的预期。 关键是我们不能依赖于当前配置在任何地方都是正确的事实。
我的事实是,您不能只将新配置与旧配置进行比较。 它可能是等效的,但同时在某处出错。 有必要检查逻辑内容。
最小程序:隔离需要的每个服务的每个参数,并检查正确性,该端口是端口,地址是地址,TTL是正数,依此类推。 并检查服务基本连接在主要端点上的关键关系。 我至少想实现这一目标。 也就是说,与前面的示例不同,此处的任务不是验证单个参数,而是使用完整的检查网络覆盖整个配置。
怎么测试呢?
public class SimpleComponent { … public void configure( final Configuration conf ) { int port = conf.getInt( "Port", -1 ); if( port < 0 ) throw new ConfigurationException(); String ip = conf.getString( "Address", null ); if( ip == null ) throw new ConfigurationException(); … } … }
我如何解决这个问题? 有一些简单的组件,在示例中,它被最大程度地简化了。 (对于那些没有遇到过Apache Commons Configuration的用户:Configuration对象就像Properties,只是它仍然具有类型化的方法getInt(),getLong()等;我们可以假定这些是小型类固醇的juProperty。)假设组件需要两个参数:例如,TCP地址和TCP端口。 我们将它们拉出并检查。 这里有四个共同的部分?

这是参数名称,类型,默认值(此处很简单:null和-1,有时会有理智的值)和一些验证。 此处的端口验证过于简单,不完整-您可以指定将通过的端口,但不是有效的网络端口。 因此,我也想改善这一时刻。 但首先,我想将这四件事变成一件事。 例如,这:
IProperty<Integer> PORT_PROPERTY = intProperty( "Port" ) .withDefaultValue( -1 ) .matchedWith( validNetworkPort() ); IProperty<String> ADDRESS_PROPERTY = stringProperty( "Address" ) .withDefaultValue( null ) .matchedWith( validIPAddress() );
这样的复合对象是对属性的描述,该属性知道其名称,默认值,可以进行验证(这里我再次使用hamcrest匹配器)。 这个对象具有如下接口:
interface IProperty<T> { FetchedValue<T> fetch( final Configuration config ) } class FetchedValue<T> { public final String propertyName; public final T propertyValue; … }
也就是说,在创建特定于特定实现的对象之后,您可以要求他从配置中提取他代表的参数。 然后,他将拉出此参数,检查过程,如果没有参数,他将提供一个默认值,导致所需的类型,并立即返回名称。
也就是说,这是参数的名称,以及服务将根据此配置请求看到的实际值。 这使您可以将多个代码行包装在一个实体中,这是我将需要的第一个简化。
解决该问题所需的第二个简化是引入一个组件,该组件的配置需要多个属性。 组件配置模型:

我们有一个使用这两个属性的组件,有一个用于其配置的模型-IConfigurationModel接口,该类实现。 IConfigurationModel会执行组件所执行的所有操作,但仅执行与配置相关的部分。 如果组件需要按特定顺序使用具有某些默认值的参数-IConfigurationModel会将此信息本身合并,并将其封装。 该组件的所有其他动作对他来说并不重要。 就配置访问而言,这是一个组件模型。

这种观点的窍门是模型是可组合的。 如果存在使用其他组件的组件,并且将它们组合在一起,则该复杂组件的模型可以以相同的方式合并两个子组件的调用结果。
即,可以构建与组件本身的层次结构平行的配置模型层次结构。 在上层模型上,调用fetch(),它将从他从配置中拉出的参数及其名称返回该工作表-恰好是相应组件实时需要的参数。 当然,如果我们正确编写了所有模型。
也就是说,任务是为有权访问配置的应用程序中的每个组件编写此类模型。 在我的应用程序中,有很多这样的组件:应用程序本身非常茂盛,但是会主动重用代码,因此仅配置了70个主要类。 对于他们,我不得不编写70个模型。
费用:
- 12项服务
- 70个可配置的类
- => 70个ConfigurationModel(〜60个很简单);
- 1-2人星期。
我只是简单地打开了带有配置自己的组件代码的屏幕,然后在下一个屏幕上为相应的ConfigurationModel编写了代码。 它们中的大多数都是琐碎的,如所示示例。 在某些情况下,存在分支和条件转换-代码变得更加分支,但是所有问题也得到解决。 在一个半到两个星期的时间内,我解决了这个问题,我对所有70个组件进行了描述。
结果,当我们将它们放在一起时,将得到以下代码:

对于每个服务/环境/等 我们采用配置模型,即该树的顶部节点,并要求从配置中获取所有内容。 此时,所有验证都将传递到内部,当每个属性从配置中拉出时,每个属性都会检查其值是否正确。 如果至少一个没有通过,则将抛出异常。 通过检查所有值是否孤立有效来获取所有代码。
服务相互依赖
我们仍然有一个问题,如何检查服务的相互依赖性。 这有点复杂,您需要查看存在哪种相互依赖性。 事实证明,相互依赖可以归结为以下事实:服务应在网络端点上“满足”。 服务A应该准确监听服务B向其发送数据包的地址,反之亦然。 在我的示例中,不同服务的配置之间的所有依赖关系都归结为这一点。 可以以一种简单的方式解决此问题:从不同的服务获取端口和地址并进行检查。 将有许多测试,它们将是庞大的。 我是一个懒人,我不想要这个。 因此,我没有这样做。
首先,我想以某种方式抽象该网络端点本身。 例如,对于TCP连接,您仅需要两个参数:地址和端口。 对于多播连接,有四个参数。 我想将其折叠成某种对象。 我是在Endpoint对象中完成此操作的,该对象隐藏了您需要的所有内容。 幻灯片是OutcomingTCPEndpoint(出站TCP网络连接)的示例。
IProperty<IEndpoint> TCP_REQUEST = outcomingTCP(
在外部,Endpoint接口由only match()方法发出,您可以在其中发出另一个Endpoint,并找出该对是否类似于一个连接的服务器和客户端部分。为什么它“像”?因为我们不知道现实中会发生什么:也许它应该正式连接到端口地址,但是在实际网络中,这些节点之间存在防火墙-我们不能仅通过配置来检查。但是我们可以确定它们是否已经正式不匹配端口/地址。然后,很可能,实际上它们也不会彼此连接。因此,我们现在有了一个复杂的属性,该属性返回Endpoint,而不是属性的原始值,即port-addresses-multicast组。在所有ConfigurationModels中,有如此复杂的属性,而不是单独的属性。这给了我们什么?这为我们提供了这种集群连接检查: ValueWithContext[] allEndpoints = flattenedConfigurationValues(environment) .filter( valueIsEndpoint() ) .toArray(); ValueWithContext[] unpairedEndpoints = Arrays.stream( allEndpoints ) .filter( e -> !hasMatchedEndpoint(e, allEndpoints) ) .toArray(); assertThat( unpairedEndpoints, emptyArray() );
在此环境的所有属性中,我们选择端点,然后简单地指定是否存在不与任何人建立联系且不与任何人建立联系的事物。所有以前的机器都允许您在几行中进行此检查。具体来说,在这里检查“每个人每个人”的复杂度为O(n ^ 2),但这并不是那么重要,因为大约有一百个端点,您甚至无法优化。也就是说,对于每个端点,我们都要遍历其他所有事物,并找出(如果有至少一个)与之连接的人。如果没有找到,很可能他应该去过那里,但是由于一个错误,他走了。通常,服务可能存在一些漏洞,这些漏洞会“伸出”,即当前应用程序外部的外部服务。此类漏洞将需要明确过滤。对于我而言,我很幸运,外部客户端通过服务本身在内部使用的相同漏洞进行连接。就网络连接而言,它是如此封闭且经济。这是测试问题的解决方案。我记得主要的任务是重构。我已经准备好用手进行重构,但是当我完成所有这些测试并开始进行测试时,我意识到我能够为更改自动进行重构。所有这些ConfigurationModel层次结构允许您执行以下操作:- 转换成另一种格式
- 执行配置请求(“此服务器上的服务使用的所有udp端口”)
- .
我可以将整个配置拖到内存中,这样每个属性都可以跟踪其来源。之后,我可以将该配置转换到内存中,然后以适合自己的顺序,以不同的顺序,以不同的格式倒入其他文件中。所以我做到了:我写了一些代码将工作表转换成我想转换的形式。实际上,我不得不做几次,因为起初不清楚哪种格式是方便和可理解的,而且我不得不多次访问以进行尝试。但这还不够。通过此构造,可以使用ConfigurationModels执行配置请求。将其提升到内存中,找出不同服务在此服务器上使用了哪些特定的UDP端口,并请求使用的端口列表以及服务说明。此外,我可以在端点上连接服务,并以图表形式显示它,并导出到.dot。其他类似的请求也很容易提出。结果就是这样一把瑞士刀-其建造成本得到了很好的回报。这就是我的终点。结论:
- 以我的经验,测试配置很重要而且很有趣。
- 悬而未决的水果很多,开始的进入门槛很低。您可以解决复杂的问题,但也有许多简单的问题。
- 如果您动脑筋,则可以获得功能强大的工具,这些工具不仅可以进行测试,而且与配置仍有很大关系。
如果您喜欢Heisenbug 2018 Piter的这份报告,请注意:12月6日至7日,下一届Heisenbug将在莫斯科举行。有关新报告的大多数描述已在会议网站上提供。从11月1日起,机票价格不断上涨-因此,现在就做出决定是有道理的。