有限战争

残酷的战争


我们有问题。 测试的问题。 测试React组件的问题,这是非常基本的。 这是关于unit testingintegration testing之间的区别。 这是关于我们所谓的单元测试和我们所谓的集成测试,大小和范围之间的区别。


这与测试本身无关,而与组件体系结构有关。 关于测试组件 ,独立库和最终应用程序之间的区别。


每个人都知道如何测试简单的组件(它们很简单),可能知道如何测试应用程序(E2E)。 如何测试有限无限的事物...


定义问题


有2种不同的方法来测试React组件- shallow以及其他所有方法,包括mountreact-testing-librarywebdriver等。 只有shallow是特殊的-其余的行为相同。


而这种区别在于大小和范围 -关于将要测试的内容,以及部分如何进行测试。


简而言之- shallow将仅记录对React.createElement的调用,而不会运行任何副作用,包括渲染DOM元素-这是React.createElement的副作用(代数)。


任何其他命令都将运行您提供的附带副作用的代码。 正如现实中那样,这就是目标。


问题出在下面: you can NOT run each and every side effect


为什么不呢


功能纯度? 纯度和不变性-今天的圣牛。 而您正在屠杀其中之一。 单元测试的公理-没有副作用,隔离,嘲笑,一切都在控制之下。


  • 但这对于... dumb components 不是问题 。 它们很笨,仅包含表示层,而没有“副作用”。


  • 但这对Containers来说是个问题 。 只要它们不笨,就包含它们想要的任何东西,并且完全具有副作用。 他们是问题!



也许,如果我们定义“正确组件”的规则,我们可以轻松地进行测试-它会指导我们并为我们提供帮助。


TRDL:有限分量

智能和哑巴组件


根据Dan Abramov的文章介绍,组件包括:


  • 关注事物的外观。
  • 可能同时包含表示性组件和容器组件** ,并且通常具有一些自己的DOM标记和样式。
  • 通常允许通过this.props.children进行遏制。
  • 与其他应用程序无关,例如Flux操作或商店。
  • 不要指定如何加载或更改数据。
  • 仅通过道具接收数据和回调。
  • 很少有自己的状态(当这样做时,它是UI状态而不是数据状态)。
  • 除非需要状态,生命周期挂钩或性能优化,否则它们被编写为功能组件。
  • 示例:页面,边栏,故事,用户信息,列表。
  • ....
  • 容器只是这些组件的数据/道具提供者。

根据起源: 在理想的应用中...
容器就是树。 组件是树叶。


在黑暗的房间里找到黑猫


这里的秘密之处是我们必须对此定义进行修改的一项更改,隐藏在“可以包含表示性和容器性组件** ”中 ,让我引用原始文章:


在本文的早期版本中,我声称呈现组件应仅包含其他呈现组件。 我不再认为是这种情况。 组件是表示组件还是容器是其实现细节。 您应该能够用容器替换演示组件,而无需修改任何呼叫站点。 因此,外观组件和容器组件都可以包含其他外观组件或容器组件。

好的,但是规则使演示组件可以测试的规则又如何- “对应用程序的其余部分没有依赖性”


不幸的是,通过将容器包括到演示组件中,您使第二个容器成为无限 ,并向应用程序的其余部分注入了依赖性。


可能那不是您打算要做的。 因此,我别无选择,只能使哑组件变得有限:


表示组件应仅包含其他表示组件


唯一的问题是,您应该问(调查当前的代码库): 如何? :tableflip:?!


如今,Presentation Presentation Components和Container不仅纠缠在一起,而且有时还没有提取为“纯”实体(您好GraphQL)。


解决方案1-DI


解决方案1很简单-不要在哑组件中包含嵌套容器-包含slots 。 只需接受“内容”(孩子们)作为道具,就可以解决问题:


  • 您无需“应用程序的其余部分”就可以测试哑巴组件
  • 您可以使用Smoke / Integration / e2e测试而不是测试来测试集成。

 // Test me with mount, with "slots emty". const PageChrome = ({children, aside}) => ( <section> <aside>{aside}</aside> {children} </section> ); // test me with shallow, or real integration test const PageChromeContainer = () => ( <PageChrome aside={<ASideContainer />}> <Page /> </PageChrome> ); 

丹本人批准:
{%twitter 1021850499618955272%}


DI(依赖注入和依赖倒置)可能是这里最可重复使用的技术,它可以使您的生活变得轻松得多。


指向此处-愚蠢的组件变得愚蠢!

解决方案2-边界


这是一个说明性的解决方案,可以扩展Solution 1仅声明所有扩展点。 只是用...包裹它们Boundary


 const Boundary = ({children}) => ( process.env.NODE_ENV === 'test' ? null : children // or `jest.mock` ); const PageChrome = () => ( <section> <aside><Boundary><ASideContainer /></Boundary></aside> <Boundary><Page /></Boundary> </section> ); 

然后-您可以将Boundary禁用为零,以减小Component范围并将其设为finite


指向此处-边界在“哑”组件层上。 哑巴组件控制着哑巴的程度。

解决方案3-层


与解决方案2相同,但具有更智能的边界,能够模拟layertier或您所说的任何内容:


 const checkTier = tier => tier === currentTier; const withTier = tier => WrapperComponent => (props) => ( (process.env.NODE_ENV !== 'test' || checkTier(tier)) && <WrapperComponent{...props} /> ); const PageChrome = () => ( <section> <aside><ASideContainer /></aside> <Page /> </section> ); const ASideContainer = withTier('UI')(...) const Page = withTier('Page')(...) const PageChromeContainer = withTier('UI')(PageChrome); 

即使这几乎与“边界”示例类似,但“哑巴”组件是“哑巴”,而“容器”控制着其他“容器”的可见性。

解决方案4-单独的问题


另一个解决方案是单独关注! 我的意思是-您已经做到了,可能是时候利用它了。


通过connect组件connect到Redux或GQL,您将生产出众所周知的容器。 我的意思是- 众所周知的名称Container(WrapperComponent) 。 您可以嘲笑他们的名字

 const PageChrome = () => ( <section> <aside><ASideContainer /></aside> <Page /> </section> ); // remove all components matching react-redux pattern reactRemock.mock(/Connect\(\w\)/) // all any other container reactRemock.mock(/Container/) 

这种方法有点粗鲁-会抹掉所有内容 ,使自己难以测试Contaiers,并且您可以使用更复杂的模拟来保留“第一个”:


 import {createElement, remock} from 'react-remock'; // initially "open" const ContainerCondition = React.createContext(true); reactRemock.mock(/Connect\(\w\)/, (type, props, children) => ( <ContainerCondition.Consumer> { opened => ( opened ? ( // "close" and render real component <ContainerCondition.Provider value={false}> {createElement(type, props, ...children)} <ContainerCondition.Provider> ) // it's "closed" : null )} </ContainerCondition.Consumer> ) 

指向此处:没有逻辑也没有Presentation,也没有容器-所有逻辑都在外部。

额外解决方案-单独的问题


您可以使用defaultProps保持紧密耦合 ,并在测试中使这些道具无效。


 const PageChrome = ({Content = Page, Aside = ASideContainer}) => ( <section> <aside><Aside/></aside> <Content/> </section> ); 

那呢


因此,我刚刚发布了一些方法来缩小任何组件的范围,并使它们更具可测试性。 从gearbox取出一个gear的简单方法。 一个简单的模式,使您的生活更轻松。


端到端测试非常棒,但是很难模拟某些条件,这些条件可能发生在深层嵌套的要素中并为它们做好了准备。 您必须具有单元测试才能模拟不同的场景。 您必须进行集成测试,以确保所有接线正确。


您知道,正如Dan在另一篇文章中所写:


例如,如果一个按钮可以处于5种不同状态之一(正常,活动,悬停,危险,禁用),则更新该按钮的代码必须对5×4 = 20种可能的过渡是正确的-或禁止其中某些过渡。 我们如何驯服可能状态的组合爆炸并使视觉输出可预测?

虽然这里正确的解决方案是状态机,但基本的要求是能够挑选单个原子或分子并对其进行操作-。


本文的要点


  1. 演示组件应仅包含其他演示组件。
  2. 容器就是树。 组件是树叶。
  3. 您不必总是不在Presentational容器中包含容器,而不必仅在测试中包含容器。

通过阅读中级文章 ,您可能会更深入地研究问题,但是在这里让我们跳过所有的困难。

PS:这是ru-habr文章habr版本的翻译

Source: https://habr.com/ru/post/zh-CN436692/


All Articles