magento2 安装测试
When I was starting my adventure with programming I thought I was a genius that could outsmart everyone out there. As a teenager I was like: “I know C++, how to display 3D models in OpenGL and enliven them with vibrant shaders. Who’s gonna stop me?”
当我开始编程时,我以为我是一个天才,可以超越所有人。 十几岁的时候,我就像:“我知道C ++,如何在OpenGL中显示3D模型并用生动的着色器使它们生动起来。 谁会阻止我?
Then when my ‘real commercial experience‘ began, life verified my skills. Beloved work turned into a flat job. I’ve started implementing bugs, some of which were very trivial like function missing return value (sic!) — it took me a while to discover code generation capabilities for my IDE. Do you feel burned out working with Magento? Now it’s time to take back control of your code and reclaim your sanity.
然后,当我开始“真正的商业经验”时,生活证明了我的技能。 心爱的工作变成了平淡的工作。 我已经开始实施错误,其中一些非常琐碎,例如函数缺少返回值(sic!)—我花了一段时间才发现IDE的代码生成功能。 您是否觉得与Magento合作感到筋疲力尽? 现在是时候收回对代码的控制并恢复理智了。
In this article you’ll learn how easy it is to build back your self-esteem with Unit Tests in Magento 2 E-commerce platform. Writing unit tests for each class was relatively easy. But like in every machine it’s not enough to have working standalone components, they also have to work together in synergy and provide greater value when used together. Moving smoothly like the gears of a Swiss watch.
在本文中,您将学习在Magento 2电子商务平台中使用单元测试来建立自尊心是多么容易。 为每个课程编写单元测试相对容易。 但是,就像在每台机器中一样,仅具有独立的组件是不够的,它们还必须协同工作,并在一起使用时才能提供更大的价值。 像瑞士手表的齿轮一样平稳地移动。
In the beginning of the 20th century Tomáš Bata, inspired by Henry Ford’s assembly line, automated shoe production. He even wrote on the walls of his factory:
在20世纪初,托马斯·巴塔(TomášBata)受亨利·福特(Henry Ford)的装配线启发,进行了自动鞋的生产。 他甚至在工厂的墙上写道:
People — think
人们认为
Machines — to labor
机器-劳动
— Gottland by Mariusz Szczygieł
— MariuszSzczygieł创作的Gottland
This approach is much cheaper to implement in the web development universe where automation is accomplished through intellectual, creative work and does not require modifications of existing physical systems at all.
这种方法在Web开发领域中的实现要便宜得多,在Web开发领域中,自动化是通过智力,创造性工作来完成的,根本不需要修改现有的物理系统。
You, your laptop and in a more sophisticated case — a CI server to run the tests — which is still cheaper than physical machines utilized in factories.
您,您的笔记本电脑以及更复杂的情况(运行测试的CI服务器)仍比工厂中使用的物理机便宜。
测试准备 (Test preparation)
Let’s dig into the code.
让我们深入研究代码。
Each test based on the PHPUnit library must extend the class:
每个基于PHPUnit库的测试都必须扩展该类:
PHPUnit\Framework\TestCase
To keep this article clean and neat we will discuss three methods of which one is only used under certain circumstances. These are:
为了使本文保持整洁,我们将讨论三种方法,其中一种仅在某些情况下才使用。 这些是:
- setUp 建立
- tearDown 拆除
- test* 测试*
建立 (setUp)
In the perfect world, our classes have a single responsibility. Their methods take simple arguments and may return a value. Especially in Magento 2 which still has places with a legacy code ported from previous versions. The vanilla PHPUnit expects you to instantiate tested classes. That’s a tedious chore to create mocks for all of the constructor arguments. Fortunately for us, Magento architects gave us a tool for easier object creation that reduces boilerplate code.
在理想世界中,我们的班级负有单一责任。 他们的方法采用简单的参数,并可能返回值。 尤其是在Magento 2中,仍然有从旧版本移植过来的遗留代码的地方。 原始PHPUnit希望您实例化经过测试的类。 为所有构造函数参数创建模拟都是繁琐的工作。 对我们来说幸运的是,Magento架构师为我们提供了一种工具,可简化对象创建并减少样板代码。
Magento\Framework\TestFramework\Unit\Helper\ObjectManager
The test helper Object Manager injects all your necessary dependencies — so you’re no longer obligated to create tons of mocks for your tested class. That speeds up test preparation a lot. Usage is straightforward. Pass $this
as the Object Manager’s first constructor referencing the test class. With the getObject
method, provide the name of the tested class and its optional constructor arguments. Only these are necessary which you want to mock. The rest is automagically injected by Magento.
测试助手对象管理器注入了所有必要的依赖关系-因此您不再必须为测试的类创建大量的模拟。 这大大加快了测试准备。 用法很简单。 传递$this
作为对象管理器的第一个引用测试类的构造函数。 使用getObject
方法,提供经过测试的类的名称及其可选的构造函数参数。 您只需要模拟这些就可以了。 其余的由Magento自动注入。
$testedClass = (new ObjectManager($this))
->getObject(OurSuperClass::class, ['config' => $config]);
拆除 (tearDown)
This method in theory should reflect reverse actions performed in reverse order in the setUp
method. In practice, when you’re creating plain objects, this part can be skipped. The PHP’s garbage collector — when enabled — usually takes care of disposing unused objects. However, when your test is allocating external resources it is a good practice just to unset these variables in the tearDown
method. When you open a file resource (e.g. with test data) you have to close it after use therefore allowing access for other processes to that file.
理论上,此方法应反映setUp
方法中以相反顺序执行的反向操作。 实际上,在创建普通对象时,可以跳过此部分。 PHP的垃圾收集器(启用后)通常负责处理未使用的对象。 但是,当您的测试分配外部资源时,一个好的做法是只在tearDown
方法中取消设置这些变量。 当您打开文件资源(例如带有测试数据)时,必须在使用后关闭它,因此允许访问该文件的其他进程。
测试* (test*)
Methods whose name begins with test
are our meat for unit testing. As you might guess, in their body we define test cases with assertions for our classes. We feed our application parts under test with positive path valid data or negative path poisonous input. Just to verify whether they handle it or fail with exception — depending on their expected behavior.
以test
开头的方法是我们用于单元测试的肉。 您可能会猜到,在他们的体内,我们为类定义了带有断言的测试案例。 我们将正路径有效数据或负路径有毒输入提供给受测的应用程序部件。 只是为了验证他们是否能够处理它或异常失败-取决于他们的预期行为。
Use assertions and data providers shipped with a testing framework. If you’ve had previous experience with the PHPUnit this part will be straightforward.
使用测试框架附带的声明和数据提供程序。 如果您以前有过PHPUnit的经验,那么这部分将很简单。
单元测试阻止的电子邮件域 (Unit Test blocked email domains)
Let’s create a real example, a bit more complex than just adding two numbers. Suppose that we want to test whether a user email domain is banned or not. List of banned email domains is configured by admin. This implies that we need two classes — one for email validation and another for retrieving invalid email domains.
让我们创建一个真实的示例,它比仅添加两个数字要复杂一些。 假设我们要测试用户电子邮件域是否被禁止。 禁止的电子邮件域列表由管理员配置。 这意味着我们需要两个类-一个用于电子邮件验证,另一个用于检索无效的电子邮件域。
- ConfigInterface ConfigInterface
- Config 设定档
- Email 电子邮件
Two classes and one interface. Purists could argue that the Email also should have an interface but I like to keep things simple. I doubt that email format could change. One statement which popped in my mind is a different validation format for example using regex instead of plain domains. Then we may utilize the strategy pattern here to handle various validation definitions.
两个类和一个接口。 纯粹主义者可能会认为电子邮件也应该有一个接口,但是我想保持简单。 我怀疑电子邮件格式是否会更改。 我想到的一个说法是一种不同的验证格式,例如使用正则表达式而不是普通域。 然后,我们可以在此处利用策略模式来处理各种验证定义。
With the ConfigInterface and mocks we are going to perform a magic trick and test Email class independently, even without defining implementation for the ConfigInterface in the Config class.
使用ConfigInterface和模拟,即使没有在Config类中定义ConfigInterface的实现,我们也将独立执行魔术和测试Email类。
namespace Tut\Registration\Model;
interface ConfigInterface
{
/**
* Retrieve email domains banned from Customer registration.
*
* @return string[]
*/
public function getBannedEmailsDomains(): array;
}
We will start with empty Email class with just one method:
我们将使用一种方法从空的Email类开始:
declare(strict_types=1);
namespace Tut\Registration\Model;/**
* Customer's email validation
*/class Email
{
/**
* Check whether email is banned
*
* @return bool
*/
public function isBanned(): bool
{
return false;
}
}
and a simple failing test:
和一个简单的失败测试:
declare(strict_types=1);
namespace Tut\Registration\Test\Unit\Model;
use Tut\Registration\Model\Email;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
use PHPUnit\Framework\TestCase;
class EmailTest extends TestCase
{
/**
* Email::isBanned should return true
*
* when domain string is banned in config.
*
* @return void
*/
public function testIsBannedReturnsTrue(): void
{
$email = (new ObjectManager($this))
->getObject(Email::class, ['email' => '[email protected]']);
$this->assertEquals(true, $email->isBanned());
}
}
Here we are instantiating the Email class with the Object Manager helper and we pass an example email argument in the array as a second parameter in the getObject method. As mentioned before, you can pass only these parameters which are required for the test. Just remember to provide arguments with appropriate types to match those declared in the constructor. Array key name is the same as the name of the argument without a dollar sign.
在这里,我们使用对象管理器帮助程序实例化Email类,并在数组中传递示例电子邮件参数作为getObject方法中的第二个参数。 如前所述,您只能传递测试所需的这些参数。 只要记住要提供具有适当类型的参数,以匹配在构造函数中声明的参数即可。 数组键名称与不带美元符号的参数名称相同。
Now, execute the PHPUnit with default configuration shipped with Magento. We may specify to run a test only for our module which takes just a milliseconds. Provide a relative path to your module as the last argument:
现在,使用Magento附带的默认配置执行PHPUnit。 我们可以指定仅对我们的模块运行测试,该测试仅需毫秒。 提供模块的相对路径作为最后一个参数:
vendor/bin/phpunit -c dev/tests/unit/phpunit.xml app/code/Tut/Registration
If you want to execute all unit test, use CLI command:
如果要执行所有单元测试,请使用CLI命令:
bin/magento dev:tests:run unit
We are sure that our test has any value when a nice failing result appears:
当出现不错的失败结果时,我们确定我们的测试具有任何价值:
PHPUnit 6.5.14 by Sebastian Bergmann and contributors.F 1 / 1 (100%)Time: 150 ms, Memory: 4.00MBThere was 1 failure:1) Tut\Registration\Test\Unit\Model\EmailTest::testIsBannedReturnsTrue
Failed asserting that false matches expected true./var/www/html/app/code/Tut/Registration/Test/Unit/Model/EmailTest.php:24FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
电子邮件课程实施 (Email class implementation)
Now we will add a basic implementation for the Email::isBanned method:
现在,我们将为Email :: isBanned方法添加一个基本实现:
declare(strict_types=1);
namespace Tut\Registration\Model;/**
* Customer's email validation
*/class Email
{
/**
* @var string
*/
private $email;
/**
* @var ConfigInterface
*/
private $config;
/**
* @param string $email
* @param ConfigInterface $config
*/
public function __construct(
string $email,
ConfigInterface $config
) {
$this->email = $email;
$this->config = $config;
}
/**
* Check whether email is banned
*
* @return bool
*/
public function isBanned(): bool
{
$domain = explode('@', $this->email);
$domain = $domain[1] ?? null;
return in_array(
$domain,
$this->config->getBannedEmailsDomains()
);
}
}
When we run the test again you might be surprised that our test is not failing during object creation. It’s Object Manager merit which has created for us a default mock for ConfigInterface. However, we want our config to return a few domains banned from registration. Mocking config is the right way to go.
当我们再次运行测试时,您可能会惊讶于我们的测试在对象创建过程中没有失败。 对象管理器的优点为我们创建了ConfigInterface的默认模拟。 但是,我们希望我们的配置返回一些禁止注册的域。 模拟配置是正确的方法。
嘲笑 (Mock)
Engineering is about finding the most optimal solution, not the perfect one. There is a movie about Nissan GTR engine assembly and in the end they run it in isolation. Car’s heart is put under stress with sensors measuring the combustion process. Engineers got output before placing it inside a car. If any issue would emerge, they don’t waste time and money on removing it and fixing or replacing it with another motor. Same concept fits perfectly for a software development, but in our case it’s applied to a smaller entity, not as complex as the combustion engine. That’s the reason why config is an interface working as the contract between parts building our application.
工程是要找到最理想的解决方案,而不是完美的解决方案。 有一部关于日产GTR发动机组件的电影 ,最后他们独立运行。 汽车的心脏受到测量燃烧过程的传感器的压力。 工程师在将其放入车内之前已获得输出。 如果出现任何问题,他们不会浪费时间和金钱来拆卸它,以及用另一台电动机进行修理或更换。 相同的概念非常适合软件开发,但是在我们的案例中,它适用于较小的实体,而不像内燃机那样复杂。 这就是为什么config是一个接口,它充当构建应用程序的各个部分之间的契约。
And now, let’s update our test. Mocking config is straightforward: create a mock by passing the name of the class or interface as the argument of the Mock Builder. In the second step, define mocked method returned value. Our application implementation would load these values from the database, but for test cases we are just specifying them in memory, as an array variable.
现在,让我们更新测试。 模拟配置很简单:通过将类或接口的名称作为模拟生成器的参数传递来创建模拟。 在第二步中,定义模拟方法的返回值。 我们的应用程序实现将从数据库中加载这些值,但是对于测试用例,我们只是在内存中将它们指定为数组变量。
$config->method('getBannedEmailsDomains')->willReturn(
[
'doe.com',
'invalid.com',
]
);
Moreover, there are two test methods — for the positive path: testIsBannedReturnsTrue
and for the failing path: testIsBannedReturnsFalse
.
此外,有两种测试方法-用于正路径: testIsBannedReturnsTrue
和用于失败路径: testIsBannedReturnsFalse
。
class EmailTest extends TestCase
{
/**
* Email::isBanned should return true
*
* when domain string is banned in config.
*
* @return void
*/
public function testIsBannedReturnsTrue(): void
{
$config = $this->getMockBuilder(ConfigInterface::class)
->getMock();
$config->method('getBannedEmailsDomains')->willReturn(
[
'doe.com',
'invalid.com',
]
);
$email = (new ObjectManager($this))->getObject(
Email::class,
[
'email' => '[email protected]',
'config' => $config
]
);
$this->assertEquals(true, $email->isBanned());
}
/**
* Email::isBanned should return false
*
* when domain string is not defined in config.
*
* @return void
*/
public function testIsBannedReturnsFalse(): void
{
$config = $this->getMockBuilder(ConfigInterface::class)
->getMock();
$config->method('getBannedEmailsDomains')->willReturn(
[
'invalid.com',
]
);
$email = (new ObjectManager($this))->getObject(
Email::class,
[
'email' => '[email protected]',
'config' => $config
]
);
$this->assertEquals(false, $email->isBanned());
}
}
Run tests again:
再次运行测试:
vendor/bin/phpunit -c dev/tests/unit/phpunit.xml app/code/Tut/Registration
And as a result, both tests are passing:
结果,两个测试都通过了:
PHPUnit 6.5.14 by Sebastian Bergmann and contributors... 2 / 2 (100%)Time: 148 ms, Memory: 4.00MBOK (2 tests, 2 assertions)
In a moment your confidence will be boosted. With these two simple tests you can safely refactor isBanned
method:
一会儿您的信心就会增强。 通过这两个简单的测试,您可以安全地重构isBanned
方法:
/**
* Check whether email is banned
*
* @return bool
*/public function isBanned(): bool
{
return in_array(
$this->getDomain(),
$this->config->getBannedEmailsDomains()
);
}/**
* Get domain for this email
*
* @return string|null
*/private function getDomain(): ?string
{
$domain = explode('@', $this->email);
return $domain[1] ?? null;
}
Now you can see how you’ve unlocked a sane iterative app development.
现在,您将看到如何解锁合理的迭代应用程序开发。
资料提供者 (Data providers)
Let’s suppose that we had many various combinations of input data and expected results. Duplicating assertions for each case would quickly bloat test methods with a boilerplate code. Luckily for us, data providers come with help. Data provider is a public function defined inside a test class which returns array of values that are passed to a test* function.
假设我们有许多输入数据和预期结果的组合。 针对每种情况重复声明将使用样板代码快速膨胀测试方法。 对我们来说幸运的是,数据提供者会提供帮助。 数据提供程序是在测试类中定义的公共函数,该函数返回传递给test *函数的值的数组。
Indicating which data provider is used for a test function is accomplished by adding @dataProvider
annotation followed by data provider function name. Arguments in the test method must match count and order specified in each row returned by our data provider.
通过添加@dataProvider
批注和数据提供者功能名称来完成,指示哪个数据提供者用于测试功能。 测试方法中的参数必须与数据提供者返回的每一行中指定的计数和顺序匹配。
/**
* Email::isBanned should return true
*
* when domain string is banned in config.
*
* @param array $bannedDomains
* @return void
* @dataProvider bannedDomains
*/public function testIsBannedReturnsTrue(array $bannedDomains): void
{
$config = $this->getMockBuilder(ConfigInterface::class)
->getMock();
$config
->method('getBannedEmailsDomains')
->willReturn($bannedDomains);
$email = (new ObjectManager($this))->getObject(Email::class, [
'email' => '[email protected]',
'config' => $config
]);
$this->assertEquals(true, $email->isBanned());
}/**
* @return array
*/public function bannedDomains(): array
{
return [
'banned domains with .com' => [
[
'doe.com',
'invalid.com',
]
],
'banned domains with .io' => [
[
'fake.io',
'test.io',
]
],
];
}
As you might have noticed, method testIsBannedReturnsTrue
retrieves a list of domains in a $bannedDomains
argument. Then this argument is used as a return result for the mocked method getBannedEmailDomains
. Do you feel how powerful data providers are? Can you imagine what else can be done with data providers? Keep in mind that the data providers are still functions.
您可能已经注意到,方法testIsBannedReturnsTrue
检索$bannedDomains
参数中的域列表。 然后将此参数用作getBannedEmailDomains
方法getBannedEmailDomains
的返回结果。 您觉得数据提供者有多强大? 您能想象数据提供者还能做什么? 请记住,数据提供程序仍在运行。
Additionally, you can add a key for each test row with a meaningful name. This helps to quickly figure out which data set was failing. Without defined keys, array will have keys defined by PHP as consecutive numbers.
此外,您可以为每个测试行添加一个有意义的名称的键。 这有助于快速找出发生故障的数据集。 没有定义的键,数组将具有由PHP定义为连续数字的键。
练习题 (Exercises)
Email class is not perfect. It has a few edge cases which might break our application. These are left for you as an exercise. Use various input combinations for your unit tests to spot them.
电子邮件课程并不完美。 它有一些边缘情况,可能会破坏我们的应用程序。 这些留给您作为练习。 对单元测试使用各种输入组合以发现它们。
最后的想法 (Final thoughts)
This is just an invitation to a testing in Magento 2. We’ve laid a foundation for further exploration of PHPUnit and Magento’s testing tools. However, try to be pragmatic. Avoid wasting time on unnecessary fancy stuff that is attractive for architecture astronauts. We all have deadlines - done is better than perfect. After all programming languages code is executed deterministically — in most circumstances. It would be a madness to write tests for our tests. Test only these parts of your application which might be violated by modifications. Determining what to test is more of an art than science and requires patience to do it right.
这只是对Magento 2中测试的邀请。我们为进一步探索PHPUnit和Magento的测试工具奠定了基础。 但是,请务实。 避免将时间浪费在不必要的花哨的东西上,这些东西对建筑宇航员很有吸引力。 我们都有最后期限-做的比完美的要好。 在大多数情况下,所有编程语言都可以确定性地执行代码。 为我们的测试编写测试太疯狂了。 仅测试应用程序的这些部分,这些部分可能会遭到修改。 确定要测试的内容是一门艺术,而不是科学,需要耐心才能正确地做。
翻译自: https://medium.com/the-innovation/reclaim-your-sanity-in-magento-2-with-unit-tests-e035f3e32410
magento2 安装测试