当前位置: 首页 > news >正文

【Code】《代码整洁之道》笔记-Chapter9-单元测试

第9章 单元测试

过去十年以来,编程专业领域进步很大。1997年时,没人听说过测试驱动开发。对于我们之中的大多数人来说,单元测试是那种用来确保程序“可运行”的用过即扔的短代码。我们辛勤地编写类和方法,再弄出一些特殊代码来测试它们。通常这些代码会是一种简单的驱动式程序,让我们能够手工与自己编写的程序交互。

我记得在20世纪90年代曾为一套嵌入式实时系统编写过C++程序。该程序是一个简单的计时器,有如下签名:

void Timer::ScheduleCommand(Command* theCommand, int milliseconds)

想法很简单;到达指定毫秒数时,在一个新线程中执行Commandexecute方法。问题在于如何测试它。

我随便写了个简单的驱动式程序,聆听来自键盘的动作。键盘输入一个字符时,它就安排5秒之后输出同样的字符。我输入了一句带节奏的歌词,然后等着5秒之后它在屏幕上重现出来。

“I . . . want-a-girl . . . just . . . like-the-girl-who-marr . . . ied . . . dear . . . old . . . dad.”[1]

在按下那些“.”键时,我真的在哼着那段旋律,当那些句点出现在屏幕上时,我又哼了一次。

那就是我的测试!我看到这法子可行,演示给同事们看,然后就把代码扔掉了。

如前文所述,我们的专业领域进步甚多。如今,我会编写测试,确保代码中每个犄角旮旯都如我所愿地工作。我会将代码和操作系统隔离开,而不是直接调用标准计时功能。我会伪造一套计时函数,这样就能全面控制时间。我会安排一些设置布尔值标识的命令,往前步进时间,查看这些标识,确保它们在我将时间调到正确值时由false变为true

有了一套运行通过的测试,我会确保任何需要用到代码的人都能方便地使用这些测试。我会确保测试和代码一起签入同一个代码包。

对,我们进步甚多,但还有很长的路要走。敏捷和TDD运动鼓舞了许多程序员编写自动化单元测试,每天还有更多人加入这个行列。但是,在争先恐后将测试加入规程中时,许多程序员遗漏了一些关于编写好测试的更细微但却重要的要点。

9.1 TDD三定律

谁都知道TDD要求我们在编写生产代码前先编写单元测试。但这条规则只是冰山之巅。看看下列3条定律。

第一定律 在编写不能通过的单元测试前,不可编写生产代码。

第二定律 只可编写刚好无法通过的单元测试,不能编译也算不通过。

第三定律 只可编写刚好足以通过当前失败测试的生产代码。

这3条定律将你限制在大概30秒一个的循环中。测试与生产代码一起编写,测试只比生产代码早写几秒。

这样写程序,我们每天就会编写数十个测试,每个月编写数百个测试,每年编写数千个测试。这样写程序,测试将覆盖所有生产代码。测试代码量足以匹敌生产代码量,导致令人生畏的管理问题。

9.2 保持测试整洁

几年前,有人请我去指导一个开发团队。那个团队认定,测试代码的维护不应遵循生产代码的质量标准。他们彼此默许在单元测试中破坏规矩。“速而不周”成了团队格言,即变量命名不用很好,测试函数不必短小和具有描述性,测试代码不必做良好设计和仔细划分,只要测试代码还能工作,只要还覆盖着生产代码,就足够好。

有些读者可能会同意这种做法。或许,在很久以前,你也用过我为那个Timer类写测试的方法。从编写那种用后即扔的测试到编写全套自动化单元测试是一大进步。所以,就像那个我指导过的团队一样,你或许也会认为脏测试好于没测试。

这个团队没有意识到的是,脏测试等同于——如果不是坏于的话——没测试。问题在于,测试必须随生产代码的演进而修改。测试越脏,就越难修改。测试代码越缠结,你就越有可能花更多时间塞进新测试,而不是编写新的生产代码。修改生产代码后,旧测试就会开始失败,而测试代码中乱七八糟的东西将阻碍代码再次通过。于是,测试变得就像是不断翻番的债务。

随着版本递进,团队维护测试代码组的代价也在上升,最终,这样的代价变成了开发者最大的抱怨对象。当经理们问及为何超支如此巨大,开发者们就归咎于测试。最后,他们只能扔掉整个测试代码组。

但是,没有了测试代码组,他们就失去了确保对代码的改动能如愿工作的能力。没有了测试代码组,他们就无法确保对系统某个部分的修改不会影响系统的其他部分。故障率开始上升。随着并非出自有意的故障越来越多,他们开始害怕做改动。他们不再清理生产代码,因为他们害怕修改带来的损害多于收益。生产代码开始腐坏。最后,他们只剩下没有测试、纷乱而缺陷缠身的生产代码,沮丧的客户,还有对测试的失望。

在某种意义上,他们说对了。测试的确让他们失望。不过是他们自己决定让测试变得乱七八糟的,而那正是失败的根源。如果他们保持测试整洁,测试就不会令他们失望,我可以拍着胸脯这么说,因为我曾经参与并指导了多个凭借整洁单元测试获得成功的团队。

故事的寓意很简单:测试代码和生产代码一样重要。测试代码可不是二等公民,它需要被思考、被设计和被照料,它该像生产代码一般保持整洁。

测试带来一切好处

如果测试不能保持整洁,你就会失去它们。没有了测试,你就会失去保证生产代码可扩展的一切要素。你没看错,正是单元测试让你的代码可扩展、可维护、可复用。原因很简单。有了测试,你就不用担心对代码的修改!没有测试,每次修改都可能带来缺陷。无论架构多有扩展性,无论设计划分得有多好,如果没有了测试,你就很难做改动,因为你担忧改动会引入不可预知的缺陷。

有了测试,愁云一扫而空。测试覆盖率越高,你就越不用担心。哪怕是对于那种架构并不优秀、设计晦涩纠缠的代码,你也能近乎没有后患地做修改。实际上,你甚至能毫无顾虑地改进架构和设计!

所以,覆盖了生产代码的自动化单元测试程序组能尽可能地保持设计和架构的整洁。测试带来了一切好处,因为测试使改动变得可能。

如果测试不干净,你改动自己代码的能力就会有所限制,而你也会开始失去改进代码结构的能力。测试越脏,代码就会变得越脏。最终,你丢失了测试,代码开始腐坏。

9.3 整洁的测试

整洁的测试有哪些要素呢?有3个要素:可读性、可读性和可读性。在单元测试中,可读性甚至比在生产代码中还重要。测试如何才能做到可读?和在其他代码中一样:明确,简洁,并有足够的表达力。在测试中,你要以尽可能少的文字表达大量内容。

我们来看看代码清单9-1中来自FitNesse的代码。这3个测试很难读懂,显然有改善空间。首先,其中有数量巨大的重复代码[G5]调用addPageassertSubString。更重要的是,代码中充满干扰测试表达力的细节。

代码清单9-1 SerializedPageResponderTest.java

public void testGetPageHieratchyAsXml() throws Exception
{crawler.addPage(root, PathParser.parse("PageOne"));crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));crawler.addPage(root, PathParser.parse("PageTwo"));request.setResource("root");request.addInput("type", "pages");Responder responder = new SerializedPageResponder();SimpleResponse response = (SimpleResponse) responder.makeResponse(new FitNesseContext(root), request);String xml = response.getContent();assertEquals("text/xml", response.getContentType());assertSubString("<name>PageOne</name>", xml);assertSubString("<name>PageTwo</name>", xml);assertSubString("<name>ChildOne</name>", xml);
}public void testGetPageHieratchyAsXmlDoesntContainSymbolicLinks() 
throws Exception
{WikiPage pageOne = crawler.addPage(root, PathParser.parse("PageOne"));crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));crawler.addPage(root, PathParser.parse("PageTwo"));PageData data = pageOne.getData();WikiPageProperties properties = data.getProperties();WikiPageProperty symLinks = properties.set(SymbolicPage.PROPERTY_NAME);symLinks.set("SymPage", "PageTwo");pageOne.commit(data);request.setResource("root");request.addInput("type", "pages");Responder responder = new SerializedPageResponder();SimpleResponse response = (SimpleResponse) responder.makeResponse(new FitNesseContext(root), request);String xml = response.getContent();assertEquals("text/xml", response.getContentType());assertSubString("<name>PageOne</name>", xml);assertSubString("<name>PageTwo</name>", xml);assertSubString("<name>ChildOne</name>", xml);assertNotSubString("SymPage", xml);
}public void testGetDataAsHtml() throws Exception
{crawler.addPage(root, PathParser.parse("TestPageOne"), "test page");request.setResource("TestPageOne");request.addInput("type", "data");Responder responder = new SerializedPageResponder();SimpleResponse response = (SimpleResponse) responder.makeResponse(new FitNesseContext(root), request);String xml = response.getContent();assertEquals("text/xml", response.getContentType());assertSubString("test page", xml);assertSubString("<Test", xml);
}

请看对PathParser的那些调用,它们将字符串转换为供爬虫使用的PagePath实体。转换与测试毫无关系,徒然混淆了代码的意图。与创建responder相关的细节,还有response的收集与转换也尽是噪声,此外还有从resource和参数构造请求URL的笨手段。(这些代码我有幸参与编写,所以可以敞开来批评。)

最终,这段代码不是设计来给人看的。可怜的读者淹没在细节的汪洋大海中,在真正用到测试之前,还得理解这些细节。

现在看看代码清单9-2中改进了的测试。这些测试还是做同样的事,不过已经被重构为更整洁和更有表达力的形式。

代码清单9-2 SerializedPageResponderTest.java(重构后)

public void testGetPageHierarchyAsXml() throws Exception {makePages("PageOne", "PageOne.ChildOne", "PageTwo");submitRequest("root", "type:pages");assertResponseIsXML();assertResponseContains("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");
}public void testSymbolicLinksAreNotInXmlPageHierarchy() throws Exception {WikiPage page = makePage("PageOne");makePages("PageOne.ChildOne", "PageTwo");addLinkTo(page, "PageTwo", "SymPage");submitRequest("root", "type:pages");assertResponseIsXML();assertResponseContains("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");assertResponseDoesNotContain("SymPage");
}public void testGetDataAsXml() throws Exception {makePageWithContent("TestPageOne", "test page");submitRequest("TestPageOne", "type:data");assertResponseIsXML();assertResponseContains("test page", "<Test");
}

这些测试显然呈现了构造-操作-检验(BUILD-OPERATE-CHECK)模式。每个测试都清晰地拆分为3个环节。第一个环节构造测试数据,第二个环节操作测试数据,第三个环节部分检验操作是否得到期望的结果。

注意,大部分恼人的细节消失了。测试直达目的,只用到那些真正需要的数据类型和函数。读测试的人应该都能够很快搞清楚状况,而不至于被细节误导或吓倒。

9.3.1 面向特定领域的测试语言

代码清单9-2中的测试展示了为测试构造一种面向特定领域的语言的技巧。我们没有直接使用程序员用来对系统进行操作的API,而是打造了一套包装这些API的函数和工具代码,这样就能更方便地编写测试,写出来的测试也更便于阅读。那正是一种测试语言,可以帮助程序员编写自己的测试,也可以帮助后来者阅读测试。

这种测试API并非起初就设计出来的,而是在对那些充满令人迷惑细节的测试代码进行后续重构时逐渐演进的。如同你看见我将代码清单9-1重构为代码清单9-2一般,守规矩的开发者也将他们的测试代码重构为更简洁和更具表达力的形式。

9.3.2 双重标准

在某种意义上,本章开始处提到的那个团队的做法是正确的。测试API中的代码与生产代码相比,的确有一套不同的工程标准。测试代码应当简单、精悍、足具表达力,但它该和生产代码一般有效。毕竟它是在测试环境而非生产环境中运行的,这两种环境有着截然不同的需求。

请看代码清单9-3中的测试。在为某个环境控制系统设计原型时,我写了这个测试。无须深入细节,你就能说出该测试是在“温度太低”时检验温度警报器、加热器和送风机是否全部打开。

代码清单9-3 EnvironmentControllerTest.java

@Test
public void turnOnLoTempAlarmAtThreashold() throws Exception {hw.setTemp(WAY_TOO_COLD);controller.tic();assertTrue(hw.heaterState());assertTrue(hw.blowerState());assertFalse(hw.coolerState());assertFalse(hw.hiTempAlarm());assertTrue(hw.loTempAlarm());
}

当然,这里也有许多细节。例如,tic函数是做什么的?实际上,在读测试时你可以不用担心这些问题,你只需考虑是否同意系统最终状态与“温度太低”的情况相符。

当你阅读这个测试时,可以留意到自己的眼光得在被检验的状态的名称与状态的意义之间来回跳转。你看到heaterState,眼光向左滑到assertTrue。你看到coolerState,眼光向左看assertFalse。这个过程既乏味又不可靠,它让测试变得难以阅读。

我大幅改进了测试的可读性,得到代码清单9-4。

代码清单9-4 EnvironmentControllerTest.java(重构后)

@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {wayTooCold();assertEquals("HBchL", hw.getState());
}

当然,我创建了一个wayTooCold函数,隐藏了tic函数的细节。不过要注意的是,assertEquals中的那个奇怪的字符串,大写表示“打开”,小写表示“关闭”,那些字符遵循以下次序:{heater, blower, cooler, hi-temp-alarm, lo-temp-alarm}

尽管这破坏了思维映射的规则,但看来它在这种情况下还是适用的。只要你明白其含义,你就能一眼看到那个字符串,并迅速译解出结果,如代码清单9-5所示。

代码清单9-5 EnvironmentControllerTest.java(扩展到更大范围)

@Test
public void turnOnCoolerAndBlowerIfTooHot() throws Exception {tooHot();assertEquals("hBChl", hw.getState());
}@Test
public void turnOnHeaterAndBlowerIfTooCold() throws Exception {tooCold();assertEquals("HBchl", hw.getState());
}@Test
public void turnOnHiTempAlarmAtThreshold() throws Exception {wayTooHot();assertEquals("hBCHl", hw.getState());
}@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {wayTooCold();assertEquals("HBchL", hw.getState());
}

代码清单9-6中给出了getState函数,注意,它的代码效率不是非常高。要提升效率,可能应该使用StringBuffer

代码清单9-6 MockControlHardware.java

public String getState() {String state = "";state += heater ? "H" : "h";state += blower ? "B" : "b";state += cooler ? "C" : "c";state += hiTempAlarm ? "H" : "h";state += loTempAlarm ? "L" : "l";return state;
}

StringBuffer有点儿丑陋。即便在生产代码中,就算代价较小,我也会避免使用StringBuffer;而且你可以看到,代码清单9-6中代码的代价的确很小。这套应用显然是嵌入式实时系统,计算机和内存资源都很有限。不过,测试环境大概完全不必做限制。

这就是双重标准。有些事你大概永远不会在生产环境中做,而在测试环境中做却完全没问题。通常这关乎内存或CPU效率的问题,不过却永远不会与整洁有关。

9.4 每个测试一个断言

有一个流派认为,JUnit中每个测试函数都应该有且只有一个断言语句。这条规则看似过于苛刻,但其好处却可以在代码清单9-5中看到。这些测试都归结为一个可快速方便地理解的结论。

代码清单9-2又如何?我们能将关于输出是XML的断言与输出包含某些子字符串的断言轻易地组合到一起,不过这样做看来毫无道理。然而,我们可以将测试分解为两个单独的测试,每个测试都有各自的断言,如代码清单9-7所示。

代码清单9-7 SerializedPageResponderTest.java(单个断言的版本)

public void testGetPageHierarchyAsXml() throws Exception {givenPages("PageOne", "PageOne.ChildOne", "PageTwo");whenRequestIsIssued("root", "type:pages");thenResponseShouldBeXML();
}public void testGetPageHierarchyHasRightTags() throws Exception {givenPages("PageOne", "PageOne.ChildOne", "PageTwo");whenRequestIsIssued("root", "type:pages");thenResponseShouldContain("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");
}

注意,我修改了那些函数的名称,以符合given-when-then约定。这让测试更易阅读。不幸的是,如此分解测试,导致了许多重复代码的出现。

可以利用模板方法(TEMPLATE METHOD)模式,将given/when部分放到基类中,将then部分放到派生类中,消除代码重复问题。或者,我们也可以创建一个完整的单独测试类,把given和when部分放到@Before函数中,把when部分放到每个@Test函数中,但对于这个小问题,这种做法看来有点儿机械。最后,我还是保留了代码清单9-2那种多个断言的形式。

我认为,单个断言是一个好准则。我通常都会创建支持这条准则的特定领域测试语言,如代码清单9-5所示。不过,我也不害怕在单个测试中放入多于一个的断言。我认为最好的说法是,单个测试中的断言数量应该最小化。

每个测试一个概念

更好一些的规则或许是每个测试函数中只测试一个概念。我们不想要超长的测试函数,测试完这个又测试那个。代码清单9-8就是那样一种测试的例子。这个测试应当拆解为3个单独测试,因为它测试了3件不同的事。如果把3件事混到一起,读者就不得不猜想每段代码出现的理由,以及那段代码到底要测试什么。

代码清单9-8

/*** Miscellaneous tests for the addMonths() method.*/
public void testAddMonths() {SerialDate d1 = SerialDate.createInstance(31, 5, 2004);SerialDate d2 = SerialDate.addMonths(1, d1);assertEquals(30, d2.getDayOfMonth());assertEquals(6, d2.getMonth());assertEquals(2004, d2.getYYYY());SerialDate d3 = SerialDate.addMonths(2, d1);assertEquals(31, d3.getDayOfMonth());assertEquals(7, d3.getMonth());assertEquals(2004, d3.getYYYY());SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1));assertEquals(30, d4.getDayOfMonth());assertEquals(7, d4.getMonth());assertEquals(2004, d4.getYYYY());
}

这3个测试函数大概应该像下面这个样子。

  • 对于某个有31天的月份(如5月)的最后一天
    • 增加一个该月最末一天为30日的月份(如6月)时,日期应该是该月的30日而非31日。
    • 增加最末月有31天的两个月时,日期应该是31日。
  • 对于某个有30天的月份(如6月)的最后一天
    • 增加一个有31天的月份时,日期应该是30日而非31日。

这样一来,你可以看到,在这些混杂的测试当中,隐藏有一条普遍规则。增加月数时,日期不能大于该月的最末一天。这意味着在2月28日增加月份数,就会得到3月28日。而这个测试应该有用,但被遗漏了。

并非是由于代码清单9-8中每个段落的多重断言导致问题。问题在于,有多个概念被测试,所以,最佳规则也许是应该尽可能减少每个概念的断言数量,每个测试函数只测试一个概念。

9.5 F.I.R.S.T.

整洁的测试还遵循以下5条规则,这5条规则的首字母构成了本节标题。

  • 快速(Fast)。测试应该够快。测试应该能快速运行。测试运行缓慢,你就不会想要频繁地运行它。如果你不频繁运行测试,就不能尽早发现问题,也无法轻易修正,从而也不能轻而易举地清理代码。最终,代码就会腐坏。

  • 独立(Independent)。测试应该相互独立。某个测试不应为下一个测试设定条件。你应该可以单独运行每个测试,以及以任何顺序运行测试。当测试互相依赖时,头一个测试没通过就会导致一连串的测试失败,使问题诊断变得困难,隐藏了下级错误。

  • 可重复(Repeatable)。测试应当可以在任何环境中重复通过。你应该能够在生产环境、质检环境中运行测试,也能够在无网络的列车上用笔记本电脑运行测试。如果测试不能在任意环境中重复,你就总会有个解释其失败的借口。当环境条件不具备时,你也会无法运行测试。

  • 自足验证(Self-Validating)。测试应该有布尔值输出。无论是通过或失败,你都不应该通过查看日志文件来确认测试是否通过。你不应该手工对比两个不同的文本文件来确认测试是否通过。如果测试不能自足验证,对失败的判断就会变得依赖主观,而运行测试也需要更长的手工操作时间。

  • 及时(Timely)。测试应及时编写。单元测试应该恰好在使其通过的生产代码之前编写。如果在编写生产代码之后编写测试,你会发现生产代码难以测试。你可能会因为某些生产代码本身难以测试而不去设计可测试的代码。

9.6 小结

我们只是触及了这个话题的表面。实际上,我认为应该为整洁的测试写上一整本书。对于项目的健康度,测试和生产代码同等重要。或许测试更为重要,因为它保证和增强了生产代码的可扩展性、可维护性和可复用性。所以,保持测试整洁吧。让测试具有表达力并短小精悍。发明作为面向特定领域语言的测试API,帮助自己编写测试。

如果你坐视测试腐坏,那么代码也会跟着腐坏。保持测试整洁吧。

相关文章:

【Code】《代码整洁之道》笔记-Chapter9-单元测试

第9章 单元测试 过去十年以来&#xff0c;编程专业领域进步很大。1997年时&#xff0c;没人听说过测试驱动开发。对于我们之中的大多数人来说&#xff0c;单元测试是那种用来确保程序“可运行”的用过即扔的短代码。我们辛勤地编写类和方法&#xff0c;再弄出一些特殊代码来测…...

数据结构-顺序表

目录 基本概念 什么是数据结构&#xff1f; 数据 结构 线性结构 树形结构 网状结构 第一章绪论 了解概念 几个概念 数据存储方式&#xff1a; 算法的五个重要特性: 算法设计的要求: 总结 线性表 线性表的定义&#xff1a; 顺序表的设计思想 定长顺序表 不定…...

端到端语音识别服务重构方案

以下是重构ASR服务架构&#xff0c;集成Whisper V3Conformer混合模型的端到端实现方案&#xff0c;经过技术增强与流程优化&#xff1a; 端到端语音识别服务重构方案 基于Whisper V3Conformer混合架构 系统架构设计 采用四层微服务架构&#xff0c;支持水平扩展与模块化部署…...

耳根圆通与禅定的交融与分野

引言 在浩瀚的修行体系中&#xff0c;耳根圆通与禅定的关系犹如月映千江——本质同一而显相各异。本文以《楞严经》为经教依据&#xff0c;结合禅宗心法与现代实证视角&#xff0c;系统解析二者在修行原理、实践路径与终极旨归中的深刻关联与微妙差异&#xff0c;揭示这一古老智…...

python基础语法:缩进规则

Python 的缩进规则是其语法的重要组成部分&#xff0c;它通过缩进来表示代码块的层次结构&#xff0c;而不是像其他语言&#xff08;如 C 或 Java&#xff09;那样使用大括号 {}。以下是 Python 缩进规则的详细说明&#xff1a; 1. 缩进的基本规则 代码块的标识&#xff1a;Pyt…...

从0到1的Python接口自动化学习路线

Python 是一门非常适合初学者且功能强大的编程语言,它在接口自动化测试领域具有广泛应用。 以下是一份针对 Python 与接口自动化测试的详细学习路线,帮助你从零开始学习并逐步掌握相关知识。 第一阶段:Python基础 目标:掌握 Python 基本语法和编程能力。 一、学习内容 1.…...

大数据(7.3)Kafka量子安全加密实践指南:构建抗量子计算攻击的消息系统

目录 一、量子计算带来的加密革命1.1 量子计算机的威胁时间表1.2 Kafka现有加密机制脆弱性分析 二、后量子加密算法选型2.1 NIST标准化算法矩阵2.2 混合加密最佳实践 三、Kafka量子安全改造方案3.1 Bouncy Castle量子安全Provider3.2 Kafka服务端配置 四、实战案例&#xff1a;…...

【11408学习记录】英语语法精讲:主从复合句之状语从句全解析——以时间状语从句为例

时间 英语语法总结—— 主从复合句状语从句从句位置从句的分类 每日一句词汇第一步&#xff1a;找谓语第二步&#xff1a;断开第三步&#xff1a;简化第一句第二句第三句第四句 英语 语法总结—— 主从复合句 状语从句 状语从句指的是一个句子作状语&#xff0c;表达“描述性…...

深度分页及优化建议

深度分页的定义 深度分页是指在分页查询中&#xff0c;当用户请求非常靠后的页面时&#xff0c;数据库需要处理大量数据&#xff0c;导致查询性能显著下降的情况。例如&#xff0c;一个查询结果有 100 万条记录&#xff0c;而用户要查询第 999 页&#xff08;每页 10 条记录&a…...

阿里云kafka集成boot在docker启动找不到kafka.client.truststore.jks文件问题

此问题困扰了我好久&#xff0c;看阿里云官方文档&#xff0c;建议配置绝对路径&#xff0c;但项目部署在docker没有绝对路径&#xff0c;开始以为配置在docker的/root下即可&#xff0c;但报找不到文件&#xff0c;后来改相对路径 ./kafka.client.truststore.jks,…/…/还是找…...

kafka 集群搭建,开启sasl认证

Kafka提供了多种认证方式来保护集群的安全性,包括以下几种常见的认证方式: SSL/TLS认证:Kafka支持使用SSL/TLS协议对网络通信进行加密和认证。通过使用SSL/TLS证书对客户端和服务器进行身份验证,可以确保通信的机密性和完整性。SASL/PLAIN认证:SASL/PLAIN是一种基于用户名…...

在 iOS 项目中,Info.plist文件用于配置应用的基本信息和权限

在 iOS 项目中,Info.plist 文件用于配置应用的基本信息和权限。以下是常见的需要添加的权限及其说明: 常见权限及说明 NSCameraUsageDescription 说明: 说明应用为何需要访问相机。示例: “我们需要访问您的相机以便您可以拍摄照片。”NSPhotoLibraryUsageDescription 说明:…...

【完整可用】使用openhtmltopdf生成PDF(带SVG)

文章目录 前言OpenHTMLToPDF 简介maven配置依赖字体文件demo代码其他资源放置截图防止maven编译字体文件 前言 AI和网上都是跑不起来或者版本过低的&#xff0c;还有各种BUG的。本文都是查阅官方文档得出的。如果你能跑起来请给个大大的赞&#xff01; OpenHTMLToPDF 简介 Ope…...

仿照管理系统布局配置

1.vue仿照snowy 配置&#xff0c;如下图&#xff1a; 2.代码实现 <template><div class"theme-settings"><!-- 导航栏 --><div class"nav-bar"><el-breadcrumb separator"/"><el-breadcrumb-item>导航设置…...

RLAgent note

OpenManus github LlamaGym github GRPO 实践 知乎&#xff1a;Deepseek R1 Zero成功复现 BabyAGI 0&#xff0c;环境 CUDA版本12.X&#xff1a;nvcc -V python 3.10&#xff1a;python -V gcc 11&#xff1a;gcc -V 1&#xff0c;安装llama-cpp-python [git | docs]…...

Python设计模式-工厂模式

一、模式定义与核心思想 工厂模式&#xff08;Factory Pattern&#xff09;属于创建型设计模式&#xff0c;其核心思想是通过一个"工厂类"来创建对象&#xff0c;而不是直接调用类的构造函数。这种模式将对象的实例化过程封装起来&#xff0c;使系统在实例化对象时能…...

Vue环境搭建:vue+idea

目录 第一章、Vue环境搭建&#xff1a;安装node2.1&#xff09;node的下载2.2&#xff09;配置node的环境变量2.3&#xff09;常见的npm命令 第二章、使用idea创建vue工程2.1&#xff09;在IDEA中设置国内镜像2.2&#xff09;在IDEA中进行脚手架安装2.3&#xff09;在IDEA中创建…...

庙算兵推:使用Streamlit框架构建了一个智能作战推演系统。

这段代码是一个完整的军事模拟应用&#xff0c;使用Streamlit框架构建了一个智能作战推演系统。该系统包括了三维地图显示、作战单位管理、应急事件处理等功能。用户可以通过界面控制推演的开始和暂停&#xff0c;调整时间加速倍率&#xff0c;并查看实时的战斗情况和系统状态。…...

APIGen-MT:高效生成多轮人机交互Agent数据的两阶段框架

APIGen-MT&#xff1a;高效生成多轮人机交互数据的两阶段框架 引言 随着人工智能技术的飞速发展&#xff0c;AI代理&#xff08;Agent&#xff09;已从简单的聊天机器人发展为能够执行复杂现实任务的系统&#xff0c;例如管理金融交易、安排预约和处理客户服务等。然而&#x…...

02-redis-数据结构实现原理

1、redis整体涉及的结构 在redis中整体是KV键值对的方式进行访问的&#xff0c;redis的查询的时间复杂度O(1)&#xff0c;底层的数据结构其实跟java中的HashMap底层实现类似&#xff0c;整体采用的是数组链表的实现方式&#xff0c;哈希冲突的时候使用的是链表法解决&#xff1…...

京华幻梦:科技自然共生诗篇

故事摘要 故事发生在现代中国北京&#xff0c;展现了未来城市的奇幻景象与科技变革。在这个充满想象的未来世界里&#xff0c;科技与自然不再对立&#xff0c;而是达成了和谐共生的美妙平衡。故宫、鸟巢、798艺术区等标志性地点&#xff0c;在科技的赋能下焕发新的生机&#x…...

python:面向对象之包

1.包的定义&#xff1a; 包就是把有联系的模块组织在一起&#xff0c;即放在同一文件夹下&#xff0c;并且在这个文件夹下创建一个__init__.py文件&#xff0c;这个文件就叫做包。 2.包的创建&#xff1a; 创建好好会自动生成一个__init__.py文件。 3.包的调用&#xff1a; …...

spring boot整合redis

spring boot整合redis 步骤&#xff1a; ① 引入redis依赖 <dependency><groupId>org.springframework.boot</group><artifactId>spring-boot-starter-data-redis</artifactId> </dependency>②在application.yml配置文件中&#xff0c…...

DIA——边缘检测

1.边缘 边缘是像素的突变位置。 2.常见边缘检测算法 通过找到一阶导数的极值点或者二阶导数的过零点来确定边缘像素的位置。边缘检测通常使用算子&#xff0c;即特定的卷积核。通过差分对离散的像素点求导&#xff0c;然后转化成卷积核进行卷积。使用卷积统一涵盖求导&…...

redis 免安装版本 启动方法 windows 安装包

redis 免安装版本 启动方法 windows 安装包 下载解压直接使用 百度网盘连接如下 链接&#xff1a;https://pan.baidu.com/s/1W4ICvdUUxkWPhK93GtdG0Q 提取码&#xff1a;vzw3 下载解压后会用32位和64位两种&#xff0c;根据自己的电脑选择 cmd命令 cd /d D:\yaochengwei\so…...

C语言练习二 进制转换

#include <stdio.h>// 强制类型转换int main(){int i 5;float j i / 2; // 只是把整形先除了 再赋值float k (float)i / 2; //先强制改成float 再赋值printf("%f\n",j);printf("%f\n",k);return 0; } #include <stdio.h> int main(){int…...

AlDente Pro for Mac电脑 充电限制保护工具

AlDente Pro for Mac电脑 充电限制保护工具 一、介绍 AlDente Pro for Mac&#xff0c;是一款充电限制保护工具&#xff0c;是可以限制最大充电百分比来保护电池的工具。锂离子和聚合物电池&#xff08;如 MacBook 中的电池&#xff09;在40&#xff05; 至 80&#xff05; 之…...

物联网卡(NB-IoT/4G)技术详解

物联网卡&#xff08;IoT SIM卡&#xff09;是专为物联网设备设计的流量卡&#xff0c;支持NB-IoT、4G Cat.1等低功耗广域网络&#xff08;LPWAN&#xff09;&#xff0c;广泛应用于智能烟感、共享设备、车联网等领域。以下是NB-IoT和4G物联网卡的对比与选型指南。 1. NB-IoT v…...

HTML5 Video (视频) 深入解析

一、引言 在当今的互联网时代,视频已经成为网站内容中不可或缺的一部分。从产品介绍、教程演示到娱乐内容,视频以其生动直观的特点吸引着大量用户。HTML5 的出现,为在网页上展示视频提供了一个标准且强大的解决方案,改变了过去依赖插件(如 Flash)来显示视频的局面。 二…...

NO.87十六届蓝桥杯备战|动态规划-完全背包|疯狂的采药|Buying Hay|纪念品(C++)

完全背包 先解决第⼀问 状态表⽰&#xff1a; dp[i][j]表⽰&#xff1a;从前i个物品中挑选&#xff0c;总体积不超过j&#xff0c;所有的选法中&#xff0c;能挑选出来的最⼤价 值。&#xff08;这⾥是和01背包⼀样哒&#xff09; 那我们的最终结果就是dp[n][V] 。状态转移⽅…...

Win11企业版安装wsl遇到的坑

起因是windows11上安装了docker desktop&#xff0c;但是启动以后显示Docker Engine stopped&#xff0c;一顿搜索。 可以参考&#xff1a;windows 11系统下打开docker 提示 docker engine stopped - DbWong_0918 - 博客园 我这边主要是检查了第2点&#xff0c;开启windows h…...

C++手撕单链表及逆序打印

在学习数据结构的过程中&#xff0c;链表是一个非常重要的基础数据结构。今天&#xff0c;我们将通过C手动实现一个单链表&#xff0c;并添加一个逆序打印的功能&#xff0c;帮助大家更好地理解链表的实现和操作。 一、链表简介 链表是一种线性数据结构&#xff0c;其中每个元…...

Kubernetes外部etcd集群的快速Docker Compose 部署

一、背景 在高可用 Kubernetes 部署中&#xff0c;需要单独部署外部 etcd 集群&#xff0c;而不是使用 kubeadm 默认在 master 节点上部署的 etcd。以下是关于这一配置场景的详细记录。 二、etcd简介 etcd 是一个高可用的分布式键值存储系统&#xff0c;主要用于存储和管理配…...

docker的目录挂载与卷映射

文章目录 一、目录挂载背景定义使用 二、卷映射背景定义区别使用docker对卷的操作 一、目录挂载 背景 上一文&#xff0c;我们提了docker exec进入容器修改页面很麻烦&#xff0c;所以在这里&#xff0c;我们学习一个新的容器使用方法&#xff0c;叫“目录挂载” 定义 长话…...

十三种物联网/通信模块综合对比——《数据手册--物联网/通信模块》

物联网&#xff0f;通信模块 名称 功能 应用场景 USB转换模块 用于将USB接口转换为其他类型的接口&#xff0c;如串口、并口等&#xff0c;实现不同设备之间的通信。 常用于计算机与外部设备&#xff08;如打印机、扫描仪等&#xff09;的连接&#xff0c;以及数据传输和设…...

IntelliJ IDEA 中安装和使用通义灵码 AI 编程助手教程

随着人工智能技术的发展&#xff0c;AI 编程助手逐渐成为提升开发效率的强大工具。通义灵码是阿里云推出的一款 AI 编程助手&#xff0c;它能够帮助开发者实现智能代码补全、代码解释、生成单元测试等功能&#xff0c;极大地提升了编程效率和代码质量。 IntelliJ IDEA 是一款广…...

工业 IOT 平台重塑锂电龙头数字化未来

在 “双碳” 目标驱动下&#xff0c;新能源锂电池产业正经历前所未有的扩张期。作为全球原材料领域的龙头企业&#xff0c;某锂电巨头在国内布局的多个生产基地却陷入 “成长的烦恼”&#xff1a;车间里工人忙着手工录入数据&#xff0c;设备运行状态靠纸质报表传递&#xff0c…...

蓝牙连接hci 命令和事件的交互

参考&#xff1a;在HCI层看蓝牙的连接过程_hci 获取蓝牙pin码-CSDN博客 我这边查看的是core 5.2 一、数据交互流程 1、ACL连接建立后的可选流程 参考蓝牙core5.2: vol2 --> PartF --> 4 1.1 AUTHENTICATION REQUESTED Authentication can be explicitly executed at …...

Maven超级详细安装部署

1.到底什么是Maven&#xff1f;搞清楚这个 Maven 是一个项目管理工具&#xff0c;主要用于 Java 项目的构建、依赖管理和文档生成。 它基于项目对象模型&#xff08;POM&#xff09;&#xff0c;通过 pom.xml 文件定义项目的配置。 &#xff08;简单说破&#xff1a;就是工程…...

OSPF不规则区域和LSA

OSPF不规则区域 1.远离骨干的非骨干区域 R1-R4四台路由器能够正常学习到彼此路由&#xff0c;但是R5不行&#xff0c;因为R5是非法ABR 解决方法&#xff1a; 1使用Tunnel隧道将AR4连接到骨干区域 &#xff08;1&#xff09; 使用隧道解决不规则区域的问题 a.可能造成选路不…...

深入了解 UI 咨询公司:数字化时代的品牌助推器

在数字化浪潮席卷全球的当下&#xff0c;用户界面&#xff08;UI&#xff09;设计已然成为企业在激烈市场竞争中脱颖而出的关键因素。UI 咨询公司应运而生&#xff0c;凭借其专业的知识与技能&#xff0c;为企业的数字化转型和品牌建设提供强大助力。 UI 咨询公司的重要性 提…...

网络建设与运维神州数码DCN sFlow网络流量信息协议

简介 用于监控网络流量信息的协议。 主要操作&#xff1a;由被监视的交换机&#xff0c;路由器把被监控的数据通过采样&#xff0c;统计等操作发送到用于监控的用户端分析器&#xff0c;由分析器对收到的数据进行用户所要求的分析&#xff0c;从而达到监控网络的目的。 各 sFlo…...

NO.88十六届蓝桥杯备战|动态规划-多重背包|摆花(C++)

多重背包 多重背包问题有两种解法&#xff1a; 按照背包问题的常规分析⽅式&#xff0c;仿照完全背包&#xff0c;第三维枚举使⽤的个数&#xff1b;利⽤⼆进制可以表⽰⼀定范围内整数的性质&#xff0c;转化成01 背包问题。 ⼩建议&#xff1a;并不是所有的多重背包问题都能…...

vue2添加背景水印-手动实现(无组件模式)

1. App.vue <template><div id"app" class"app"><router-view></router-view></div> </template><script> export default {mounted() {this.updateWatermark();// 监听路由变化this.$router.afterEach(() >…...

华为数通Datacom认证考试难度怎么样?

华为数通Datatcom认证是华为针对数据通信领域推出的技术认证体系&#xff0c;分为‌HCIA&#xff08;初级&#xff09;、HCIP&#xff08;中级&#xff09;、HCIE&#xff08;专家级&#xff09;‌三个等级&#xff0c;考试难度逐级递增&#xff0c;对考生的理论知识和实践能力…...

一文读懂WPF系列之常用控件以及样式

WPF控件 控件分类概览常用控件常用控件代码示例和效果 样式与模板应用样式定义​​方式行内样式​​页面/窗口级资源样式&#xff08;Local Resource&#xff09;应用程序全局资源独立资源字典&#xff08;ResourceDictionary&#xff09;控件模板&#xff08;ControlTemplate&…...

代码随想录算法训练营第十四天

LeetCode题目: 513. 找树左下角的值112. 路径总和106. 从中序与后序遍历序列构造二叉树 其他: 今日总结 往期打卡 513. 找树左下角的值 跳转: 513. 找树左下角的值 学习: 代码随想录公开讲解 问题: 给定一个二叉树的 根节点 root&#xff0c;请找出该二叉树的 最底层 最左边…...

国产信创数据库:PolarDB 分布式版 V2.0,支持集中分布式一体化

阿里云PolarDB数据库管理软件&#xff08;分布式版&#xff09;V2.0 &#xff0c;安全可靠的集中分布式一体化数据库管理软件。点此查看详情https://www.aliyun.com/activity/database/polardbx-v2?spma2c6h.13046898.publish-article.8.44146ffaE0lEWT 立即咨询专家&#xf…...

【教学类-102-07】剪纸图案全套代码07——Python点状虚线优化版本+制作1图2图6图

背景需求: 我觉得这个代码里面的输入信息分离太远(42行和241行),想重新优化一下 【教学类-102-05】蛋糕剪纸图案(留白边、沿线剪)04——Python白色(255)图片转为透明png再制作“点状边框和虚线边框”-CSDN博客文章浏览阅读864次,点赞14次,收藏27次。【教学类-102-0…...

基于VSCode的Qt开发‘#include ui_test.h’报错没有该文件

笔者在基于VSCode进行Qt开发时&#xff0c;test.ui文件是在Qt软件中绘制的&#xff0c;导致本项目无法使用这个ui文件&#xff0c;报错如标题。事实上&#xff0c;本工程中也确实没有生成这个头文件。出现这个错误的原因是ui文件没有被编译为c头文件。 要生成 ui_test.h 文件&…...