bdd测试
I’ll be honest, I don’t do much testing. When it’s really necessary and I’m working on big enterprise projects, I do, but in general, my personal projects are usually one-man-army proofs of concept, or fixes on already tested apps.
老实说,我没有做太多测试。 当确实有必要并且我正在从事大型企业项目时, 我会这样做 ,但总的来说,我的个人项目通常是一臂之力的概念证明,或者是对已经测试过的应用程序的修复。
We’ve done our share of testing posts here at SitePoint, with more coming soon, but I wanted to show you a relatively new testing tool I found that caught my attention because of how unconventional it seemed.
我们已经在SitePoint上完成了我们的测试文章共享,还有更多即将发布,但我想向您展示一个相对较新的测试工具,因为它看起来太过传统,因此引起了我的注意。
Peridot is a BDD testing framework (so behavior driven testing) but for your units of code – not your business logic.
Peridot是一个BDD测试框架(因此是行为驱动的测试 ),但是针对您的代码单元-而非您的业务逻辑。
Wait, what?
等一下
Yes.
是。
If you’re familiar with Behat, you’ll recognize this syntax (it should be fairly readable even if you’re not familiar with it):
如果您熟悉Behat ,您将认识到以下语法(即使您不熟悉它,它也应该相当可读):
Feature: adding a todo As a user I want my todos to be persisted So I don't have to retype them Scenario: adding a todo Given I am on "/" When I fill in "todo" with "Get groceries" And I press "add" And I reload the page Then I should see "Get groceries" Scenario: adding a duplicate todo Given I have a done todo "Pick up dinner" And I am on "/" When I fill in "todo" with "Pick up dinner" And I press "add" Then I should see "Todo already exists" after waiting And I should see 1 "#todos li" elementsThe individual phrases are defined in FeatureContext classes like so:
在FeatureContext类中定义了各个短语,如下所示:
/** * @Then I should see :arg1 after waiting */ public function iShouldSeeAfterWaiting($text) { $this->getSession()->wait(10000, "document.documentElement.innerHTML.indexOf('$text') > -1"); $this->assertPageContainsText($text); } /** * @Given I have a todo :arg1 */ public function iHaveATodo($todoText) { $collection = self::getTodoCollection(); $collection->insert(['label' => $todoText, 'done' => false]); }The framework recognizes them, substitutes the arguments for their values, and tests the conditions.
框架可以识别它们,将参数替换为其值,然后测试条件。
But these are called “user stories” – they are what the user does, or is expected to do. Behat will literally open a browser when needed, test things, and report back. How could we possibly apply this to units of code?
但是这些被称为“用户故事” –它们是用户所做的事情或被期望做的事情。 Behat会在需要时从字面上打开浏览器,进行测试并进行报告。 我们如何将其应用于代码单元?
Like so:
像这样:
describe('ArrayObject', function() { beforeEach(function() { $this->arrayObject = new ArrayObject(['one', 'two', 'three']); }); describe('->count()', function() { it('should return the number of items', function() { $count = $this->arrayObject->count(); assert($count === 3, 'expected 3'); }); }); });In a similarly story-based style with cascading nested contexts, Peridot will “describe” and ArrayObject‘s count() method (notice how it first describes the ArrayObject, then describes the method later), and add an it for an explanation on what the described entity should do.
以类似的基于故事的样式,带有层叠的嵌套上下文,Peridot将“描述”和ArrayObject的count()方法(请注意,它首先描述ArrayObject,然后再描述该方法),并添加it以解释什么所述实体应该这样做。
The nested context approach also allows you to execute some bootstrapping logic before each entity’s creation, or before each sub-entity’s creation, making the testing framework incredibly flexible. That’s what the beforeEach function does – it initiates a new ArrayObject with three elements, so the count is always executed on the proper instance.
嵌套上下文方法还允许您在每个实体创建之前或在每个子实体创建之前执行一些自举逻辑,从而使测试框架变得异常灵活。 这就是beforeEach函数的功能–它使用三个元素启动一个新的ArrayObject ,因此计数始终在适当的实例上执行。
Ok so… what? It’s just an alternative syntax for PHPUnit? Well, yes and no. While it is focused on units and being able to test them in a story-based way, you can also see it as a broker language between clients and developers. That’s what makes Behat so appealing, too – if you give a Behat feature to a client, they will immediately be able to tell what it means and what it’s supposed to test. With a little bit of training, they’ll even be able to write more stories for you – and clients writing the tests for their own app is the holy grail of every developer’s project, I’m sure you agree.
好吧...什么? 这只是PHPUnit的替代语法吗? 好吧,是的,不是。 虽然它的重点是单位和能够测试他们的故事为基础的方式,你也可以把它看作是客户和开发商之间的经纪人语言。 这就是Behat如此吸引人的原因–如果您将Behat功能提供给客户,他们将立即能够分辨出Behat的含义以及应该进行的测试。 经过一点点的培训,他们甚至可以为您编写更多故事-并且,为自己的应用程序编写测试的客户是每个开发人员项目的圣杯,我相信您同意。
Peridot is similar in that it offers a human readable way to test units – it is still PHP syntax, but its phrasing and nesting make it user-friendly enough to be understood by the client. And the phrasing also lends itself well to some really verbose reporting:
Peridot的相似之处在于它提供了一种易于理解的方式来测试单元-它仍然是PHP语法,但是其措词和嵌套使其对用户友好,足以被客户端理解。 措辞也很适合进行一些非常冗长的报告 :
Peridot also offers “just works” concurrency with the concurrency plugin.
Peridot还通过并发插件提供了“正常工作”并发。
This is very handy when you’re testing with database connections, remote calls, or browser actions – all those are very slow, and executing them one after the other would take ages on any system. Instead, the concurrency plugin makes sure tests are executed in separate worker processes, spawned as needed. Once done, each worker communicates back to the parent process with the results, speeding up testing dramatically. There’s no configuration necessary, other than activating a plugin and passing along a flag (--concurrent and --processes) when launching the tests.
当您测试数据库连接,远程调用或浏览器操作时,这非常方便-所有这些操作都很慢,并且在任何系统上一个接一个地执行它们会花费很多时间。 相反,并发插件可确保测试在单独的工作进程中执行,并根据需要生成。 完成后,每个工作人员都会将结果与父流程进行沟通,从而极大地加快了测试速度。 除了激活插件并在启动测试时传递标记( --concurrent和--processes )之外,没有其他必要的配置。
It’s important to note that one should be careful about tests that might depend on one another in such cases – for example, if you’re executing tests that do something in a database, it wouldn’t make sense to have them all do something on the same database, because they’d probably end up in conflicts. Instead, the concurrency plugin makes available the ID of the process that’s currently executing the spec, which makes it easy to create process-specific databases during tests, keeping all specs independent and safe:
重要的是要注意,在这种情况下,应该谨慎对待可能相互依赖的测试-例如,如果您正在执行的测试在数据库中执行某项操作,那么让它们全部在某处执行某项操作就没有意义了。相同的数据库,因为它们可能最终会发生冲突。 相反,并发插件提供了当前正在执行规范的流程的ID,这使在测试过程中轻松创建特定于流程的数据库变得容易,从而使所有规范保持独立和安全:
$id = getenv('PERIDOT_TEST_TOKEN'); $dbname = "mydb_$id";To rival PHPUnit, Peridot also offers a way to completely skip some tests, and to only run others. If you prefix a function with f, the spec being tested will become focused. That’s a fancy way of saying only that test will be executed, and everything not f-ed in that suite will not run. Helpful when fine tuning a test.
为了与PHPUnit竞争,Peridot还提供了一种完全跳过某些测试而仅运行其他测试的方法。 如果为函数加上f前缀,则要测试的规范将变得集中 。 这是只表示测试将被执行的一个奇特的方式,一切不f -ed在该套件将无法运行 。 在微调测试时很有用。
<?php describe('A suite with nested suites', function() { fcontext('A focused suite', function() { it('should execute this spec', function() { // ... }); it('should also execute this spec', function() { // ... }); }); describe('An unfocused suite', function() { it('should not execute this spec', function() { // ... }); }); });In other cases, you’ll want to skip a test and mark it as temporarily unimportant or work-in-progress. When would you do this? For example, when a particularly awful implementation of the “drop-in-replacement-but-not-really-drop-in-if-we-are-being-honest” JSON extension for PHP broke my Diffbot PHP Client tests, I could do little else but mark it as skipped and wait for an extension that actually worked. Prefixing with x in Peridot is the same as marking the test skipped in PHPUnit.
在其他情况下,您可能希望跳过测试并将其标记为暂时不重要或正在进行中。 你什么时候会做? 例如,当PHP的JSON扩展的“替换掉但不能真正放下如果我们真的很诚实”的特别糟糕的实现破坏了我的Diffbot PHP Client测试时,我可以除了将其标记为已跳过,并等待实际起作用的扩展程序外,不执行其他任何操作。 在Peridot中使用x前缀与在PHPUnit中标记跳过的测试相同。
xdescribe('A pending suite', function() { xcontext('when using a pending context', function() { xit('should have a pending spec', function() { // ... }); }); });These two features in combination are able to rival similar features of other testing frameworks.
这两个功能的组合能够与其他测试框架的类似功能相媲美。
Peridot is highly extensible via plugins and events that fire at certain stages of the testing process.
Peridot可通过在测试过程的某些阶段触发的插件和事件进行高度扩展。
The events can be listened for in the main peridot.php file, and their full list is available here. With such an open architecture, all sorts of plugins can be easily developed. The Yo Plugin, for example, will send a Yo! notification via the Yo service when tests pass or fail (depending on yo [sic] configuration). The Watcher plugin will monitor your test files and re-run the suite when changes have been detected (compatibility depends on OS – please use non-Windows. Homestead Improved would be best).
可以在peridot.php主文件中侦听事件,并且可以在此处找到其完整列表。 通过这样的开放架构,可以轻松开发各种插件。 例如, Yo插件将发送Yo! 测试通过或失败时通过Yo服务发出通知(取决于yo [sic]配置)。 当检测到更改时, Watcher插件将监视您的测试文件并重新运行套件(兼容性取决于操作系统-请使用非Windows。Homestead最好是改进的 )。
With a powerful event architecture, simple syntax, and effective structure, Peridot also lends itself perfectly to integration with traditional code coverage reporting tools.
凭借强大的事件架构,简单的语法和有效的结构,Peridot还非常适合与传统代码覆盖率报告工具集成。
Here’s how to integrate it with PHPUnit’s code coverage report.
这是将其与PHPUnit的代码覆盖率报告集成的方法。
If you don’t like the describe-it syntax and aim to replicate something that feels more at home, or something more fitting for your business case, custom DSLs (Domain Specific Languages) can be developed as a replacement or augmentation. With custom DSLs, this becomes possible in Peridot:
如果您不喜欢describe-it语法,并且打算复制某种感觉更像是在家中的东西,或者更适合您的业务案例,则可以开发自定义DSL(特定于域的语言)作为替代或扩充。 使用自定义DSL,这在Peridot中成为可能:
Feature("chdir"," As a PHP user I need to be able to change the current working directory", function () { Scenario(function () { Given('I am in this directory', function () { chdir(__DIR__); }); When('I run getcwd()', function () { $this->cwd = getcwd(); }); Then('I should get this directory', function () { if ($this->cwd != __DIR__) { throw new \Exception("Should be current directory"); } }); }); });A full example of how to make this happen is available here.
有关如何实现此目标的完整示例,请参见此处 。
It is also possible to natively extend the scope of a test. For example, if we wanted to be able to use a webdriver (web browser for testing URLs) in our tests, we could activate it by installing WebDriver like so:
也可以在本地扩展测试范围。 例如,如果我们希望能够在测试中使用WebDriver(用于测试URL的Web浏览器),则可以通过安装WebDriver来激活它,如下所示:
composer require facebook/webdriver peridot-php/webdriver-managerThen, you create a class that extends Peridot’s Scope, so it can be tied into the tests:
然后,创建一个扩展Peridot范围的类,以便可以将其绑定到测试中:
<?php // src/Example/WebDriverScope namespace Peridot\Example; use Evenement\EventEmitter; use Peridot\Core\Scope; class WebDriverScope extends Scope { /** * @var \RemoteWebDriver */ protected $driver; /** * @var \Evenement\EventEmitter */ protected $emitter; /** * @param \RemoteWebDriver $driver */ public function __construct(\RemoteWebDriver $driver, EventEmitter $emitter) { $this->driver = $driver; $this->emitter = $emitter; //when the runner has finished lets quit the driver $this->emitter->on('runner.end', function() { $this->driver->quit(); }); } /** * Add a getPage method to our tests * * @param $url */ public function getPage($url) { $this->driver->get($url); } /** * Adds a findElementById method to our tests * * @param $id * @return \WebDriverElement */ public function findElementById($id) { return $this->driver->findElement(\WebDriverBy::id($id)); } }This is then activated in the main peridot.php file:
然后在主要的peridot.php文件中将其激活:
<?php // peridot.php use Peridot\Core\Suite; use Peridot\Example\WebDriverScope; require_once __DIR__ . '/vendor/autoload.php'; return function($emitter) { //create a single WebDriverScope to port around $driver = RemoteWebDriver::create('http://localhost:4444/wd/hub', array( WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::FIREFOX )); $webDriverScope = new WebDriverScope($driver, $emitter); /** * We want all suites and their children to have the functionality provided * by WebDriverScope, so we hook into the suite.start event. Suites will pass their child * scopes to all child tests and suites. */ $emitter->on('suite.start', function(Suite $suite) use ($webDriverScope) { $scope = $suite->getScope(); $scope->peridotAddChildScope($webDriverScope); }); };… and suddenly available in tests!
……并突然在测试中可用!
<?php describe('The home page', function() { it('should have a greeting', function() { $this->getPage('http://localhost:4000'); $greeting = $this->findElementById('greeting'); assert($greeting->getText() === "Hello", "should be Hello"); }); });Notice how we can even fully replicate Behat’s behavior with this!
注意,我们如何甚至可以完全复制Behat的行为!
More info about scopes here.
有关范围的更多信息,请参见此处 。
Peridot is a way to make your units of code come to life, following in the steps of the app itself when it’s being BDD-tested. On its own, it’s a great unit testing tool, but in tandem with something like Behat, it can cover the whole app rather nicely.
Peridot是使您的代码单元栩栩如生的一种方法,可以在对应用程序进行BDD测试时遵循其本身的步骤。 它本身就是一个很棒的单元测试工具,但与Behat之类的东西配合使用,它可以很好地覆盖整个应用程序。
The describe-it syntax is a bit odd to us who aren’t familiar with other tools using it, but it matches the context in which it’s being executed (describing the units) so meaning-wise, it works rather well. Besides – one can make their own DSL if describe-it doesn’t feel good.
对不熟悉使用它的其他工具的我们来说, describe-it语法有点奇怪,但是它与正在执行它的上下文(描述单元)相匹配,因此在意义上来说,它工作得很好。 此外–如果describe-it不好,可以制作自己的DSL,感觉不好。
Including Peridot, we now have four major players on the testing market: PHPUnit, Behat, PHPSpec, and Peridot.
包括Peridot在内,我们现在在测试市场上有四个主要参与者:PHPUnit,Behat,PHPSpec和Peridot。
Which of these do you use? Why? Do you use a combination? If so, why that specific combination? Can you show us examples? We’d love to take a look at your setup and your code. Tell us in the comments below!
您使用哪一个? 为什么? 您使用组合吗? 如果是这样,为什么要这样组合? 能给我们看看例子吗? 我们很乐意查看您的设置和代码。 在下面的评论中告诉我们!
The code examples above were taken from Peridot’s todo app example
上面的代码示例摘自Peridot的todo应用示例
翻译自: https://www.sitepoint.com/testing-frenzy-can-we-bdd-test-the-units/
bdd测试
相关资源:Go-Spec是一个简单的BDD风格的测试组织者采用标准库Go测试包