AWS Lambda —无服务器— Python — DEVOPS (AWS Lambda — serverless — Python — DEVOPS)
Much like software applications, infrastructure provisioning has moved away from monolithic solutions, shifting focus towards decoupled components and utilisation of public service APIs. Tasks that traditionally would have required a great deal of orchestration and heavy tooling have transformed into lightweight event-driven services. Frameworks like the AWS Serverless Application Model (AWS SAM) have come a long way and make it easy to implement complex applications in a “microservice-style”, often with little more than a few Lambdas as building blocks.
与软件应用程序非常相似,基础结构配置已从单片解决方案转移到了分离的组件和公共服务API的利用上。 传统上需要大量编排和重型工具的任务已转变为轻量级事件驱动的服务。 诸如AWS无服务器应用程序模型 (AWS SAM)之类的框架已经走了很长一段路,并且使以“微服务风格”轻松实现复杂的应用程序变得容易,通常只有很少的Lambda作为构建块。
In the previous article, I’ve shared my thoughts on implementing readable, testable and maintainable AWS Lambdas. In this article, I’ll write about unit-testing the code.
在上一篇文章中 ,我分享了有关实现可读,可测试和可维护的AWS Lambda的想法。 在本文中,我将介绍有关对代码进行单元测试的内容 。
From my own experience, I know that unit tests can be tricky for many DevOps projects — Which seems to be mostly due to the mechanics of writing good tests, as well as people’s creativity when it comes to why it’s just not the right time for it :)
根据我自己的经验,我知道对于许多DevOps项目而言,单元测试可能会很棘手-这似乎主要是由于编写好的测试的机制以及人们在不恰当的时机上的创造力所致:)
This is not the place to argue against ‘there is no time for tests’ (for the record: I wholeheartedly disagree), so let me move on to demonstrate that writing meaningful tests for AWS Lambda is not difficult at all.
这里不是反对“没有时间进行测试”的地方(记录:我完全不同意),所以让我继续说明为AWS Lambda编写有意义的测试一点都不困难。
期待什么 (What to expect)
I will be using the same exemplary problem statement than before (“Restricting inbound and outbound traffic for default security groups”), but will now focus at the testing side of things.
我将使用与以前相同的示例性问题陈述(“ 为默认安全组限制入站和出站流量” ),但现在将重点放在测试方面。
The complete working example can be found on GitHub:
完整的工作示例可以在GitHub上找到:
github.com/jangroth/writing-aws-lambdas-in-python
github.com/jangroth/writing-aws-lambdas-in-python
All source code examples are taken from there. You are welcome to follow along by setting up the project yourself.
所有源代码示例均从此处获取。 欢迎您自己设置该项目。
编写易于阅读和维护的测试 (Writing tests that are easy to read and to maintain)
Let’s start by looking at the setup and structure of tests. To me, the benchmark for a good test is how well it covers the source code and how easy it is to read.
让我们从测试的设置和结构开始。 对我来说,一个好的测试的基准是它对源代码的覆盖程度以及阅读的容易程度。
I’m using pytest
for the examples, mostly because I like the modern syntax and fixtures as a technique to help to keep tests readable. However, using unittest
from the Python SDK is completely acceptable and only requires minor changes.
我将pytest
用于示例,主要是因为我喜欢现代语法和固定装置作为一种有助于保持测试可读性的技术。 但是,使用Python SDK中的unittest
是完全可以接受的,只需要进行很小的更改即可。
1. Make tests easy to run—and hard to ignore
1.使测试易于运行,并且难以忽略
Here is something that I think the Java ecosystem does a little better than Python: Projects usually follow the same directory layout, and by default building a project will already include running all tests, simply because build tools follow conventions and know where to find everything.
我认为Java生态系统的功能要比Python好一些:项目通常遵循相同的目录布局,并且默认情况下,构建项目将已经包括运行所有测试,这仅仅是因为构建工具遵循约定并知道从何处查找所有内容。
In Python, the tooling landscape is different, and there is no one single directory layout that most projects implement. Often, this means that setting up a project and figuring out how to run the tests is up to the quality of the documentation.
在Python中,工具环境是不同的,并且大多数项目都没有实现任何单一的目录布局。 通常,这意味着设置项目并弄清楚如何运行测试取决于文档的质量。
If you are not already set with your build system, I recommend implementing a makefile with an explicit test
-target. While there are many different build systems out there, from my experience makefiles are widely understood and easy to install.
如果尚未设置构建系统,则建议使用显式的test
-target实现makefile。 尽管有许多不同的构建系统,但根据我的经验,makefile被广泛理解并且易于安装。
check: ## Run linters
...test: check ## Run tests
PYTHONPATH=./src pytest --cov=src --cov-report term-missing
@echo '*** all tests passing ***'deploy: test [...] ## Deploy project
...[→ source]
If you already have a build system in place, by all means, use that. Just make sure it’s running your tests both explicitly on a test
target and implicitly on the build and deploy targets.
如果您已经有了一个构建系统,请使用它。 只要确保它既在test
目标上显式运行,又在构建和部署目标上隐式运行测试。
2. Mock out external dependencies
2.模拟出外部依赖
In the first article, I mentioned that using classes and moving the initialisation of external dependencies into the constructor would simplify the creation of test objects. Here is how it works:
在第一篇文章中 ,我提到使用类并将外部依赖项的初始化移动到构造函数中将简化测试对象的创建。 下面是它的工作原理:
In Python, the typical way of instantiating an object is by invoking its constructor:
在Python中,实例化对象的典型方法是调用其构造函数:
obj = RevokeDefaultSg()class RevokeDefaultSg:
def __init__(self, region="ap-southeast-2"):
self.logger = logging.getLogger(...)
self.ec2_client = boto3.client(...)
self.ec2_resource = boto3.resource(...)
...[→ source]
This code will initialize logger
and boto
helpers.
此代码将初始化logger
和boto
帮助器。
For tests, this is problematic, because we do not want to call out into AWS from unit tests. Instead, we can use an alternative way to instantiate objects: In Python, the __new__
-method creates an instance of a class without invoking the constructor (documentation). In tests, this allows to create a ‘skeleton’ instance that has its individual instance attributes replaced by mocks:
对于测试,这是有问题的,因为我们不想从单元测试中调用AWS。 相反,我们可以使用另一种实例化对象的方法:在Python中, __new__
-method创建类的实例,而无需调用构造函数( documentation )。 在测试中,这允许创建一个“骨架”实例,该实例的单独实例属性被模拟替换:
def obj():
obj = RevokeDefaultSg.__new__(RevokeDefaultSg)
obj.logger = MagicMock()
obj.ec2_client = MagicMock()
obj.ec2_resource = MagicMock()
return obj[→ source]
A concrete test can then use this skeleton object (more on fixture
in the next chapter) and set up the mocks according to the test case:
然后,一个具体的测试可以使用这个骨架对象(在下一章的fixture
中有更多介绍),并根据测试用例设置模拟:
@fixture
def obj():
...def test_should_tag_if_ingress_was_revoked(obj):
mock_sg = MagicMock()
mock_sg.ip_permissions = "ingress"
mock_sg.ip_permissions_egress = None
obj.ec2_resource.SecurityGroup.return_value = mock_sg obj._revoke_and_tag(TEST_SG)
...[→ source]
This is how both execution paths for _revoke_and_tag()
look for original and test invocation side by side:
这是_revoke_and_tag()
两个执行路径如何寻找原始路径并_revoke_and_tag()
测试调用的方式:
Looping back to the initial statement — the fundamental idea of this strategy is → identifying things that are hard to set up in unit-tests, →bundling them in a single location, →bypassing invocation for testing and →injecting mocks where required.
回到最初的陈述—该策略的基本思想是→ 识别难以在单元测试中设置的事物, → 将它们捆绑在一个位置,→ 绕过测试调用,并在需要时注入模拟。
A variant of this approach is patching objects or modules using @patch
rather than bypassing code execution with __new__
. While the results are similar, I prefer the constructor-based approach. That’s probably my Java development background as well as a lack of understanding of Python’s import
name-spacing model, which usually leaves me without a clue how to debug patches if they don’t do what they should. For me, bypassing initialization just works.
这种方法的一个变体是使用修补对象或模块@patch
而不是绕过执行代码__new__
。 尽管结果相似,但我更喜欢基于构造函数的方法。 那可能是我的Java开发背景以及对Python import
名称间隔模型的了解不足,这通常使我不知道如何调试补丁(如果补丁未按其要求进行调试)。 对我来说,绕过初始化就可以了。
3. Create test data in re-usable helper methods
3.以可重用的帮助器方法创建测试数据
Almost always tests need data to process.
几乎总是测试需要数据来处理。
For example, if we want to test process_event()
, we must provide it with a sample Lambda event
that it can run against:
例如,如果要测试process_event()
,则必须为其提供示例Lambda event
,该event
可以针对以下event
运行:
def process_event(self, event):
sg_id = self._extract_sg_id(event)
if self._is_default_sg(sg_id):
self.logger.info(...)
self._revoke_and_tag(sg_id)
else:
self.logger.info(...)
return 'SUCCESS'[→ source]
A sample event isn’t complicated and could easily be created in the test itself:
样本事件并不复杂,可以在测试本身中轻松创建:
"id": "12345678-b00a-ede7-937b-b4da1faf5b81",
"detail": {
"eventVersion": "1.05",
"eventTime": "2020-02-05T12:34:56Z",
"eventSource": "ec2.amazonaws.com",
"eventName": "AuthorizeSecurityGroupIngress",
"eventID": "12345678-6466-4720-955e-e342e782d405",
"eventType": "AwsApiCall",
"requestParameters": {
"groupId": "sg-123"
}
}
However, events aren’t exactly small and have a lot more data than we are interested in, so creating them on a per-test base would add noise and repetition. A better strategy is implementing dedicated helper methods for test data. For more complex scenarios different helpers can be combined or can build on each other.
但是,事件并不是很小,并且数据比我们感兴趣的要多得多,因此在每次测试的基础上创建事件会增加噪音和重复性。 更好的策略是对测试数据实施专用的辅助方法。 对于更复杂的场景,可以将不同的助手组合在一起,也可以彼此建立。
Conveniently, pytest
has a handy feature called ‘fixture’, which allows to annotate a method and have its output injected into test cases. This example uses two fixtures — one for the test event and one for the skeleton object (see issue above):
方便地, pytest
具有一个方便的功能,称为“ fixture ”,它可以注释方法并将其输出注入到测试用例中。 此示例使用了两种固定装置-一种用于测试事件,另一种用于骨架对象(请参见上面的问题):
@fixture
def good_event():
return {
"id": "3469fd4b-b00a-ede7-937b-b4da1faf5b81",
"detail": {
...
}
}
}@fixture
def obj():
obj = RevokeDefaultSg.__new__(RevokeDefaultSg) ...def test_that_uses_good_event(obj, good_event):
...
obj.process_event(good_event)
...[→ source]
Whether you use fixtures or prefer to call methods explicitly, having dedicated helpers for test data creation is a clean technique to keep noise out of the test itself and to allow re-use across multiple tests.
无论您使用固定装置还是更喜欢显式调用方法,使用专用的助手来创建测试数据都是一种干净的技术,可将噪声排除在测试本身之外,并允许在多个测试中重复使用。
4. Test one thing at a time and mock out complexity
4.一次测试一件事并模拟复杂性
It can be tempting to squeeze multiple aspects into one test case and to fire off a whole series of assertions at the end — But usually, that’s not a good idea: Complex tests can be a nightmare to understand and to maintain.
将多个方面压缩到一个测试用例中并最终发出一系列断言可能很诱人-但是通常,这不是一个好主意:复杂的测试可能是理解和维护的噩梦。
When it comes to testing, readability counts at least as much as for production code; or maybe even more so, as tests are often written in a ‘fire and forget’ style. Focusing on a single condition per test case makes it much easier to understand the test and to grasp the problem in case of failure.
在测试方面,可读性至少与生产代码一样重要。 甚至可能更是如此,因为测试通常以“即发即弃”风格编写。 每个测试用例只关注一个条件,就可以更轻松地理解测试并在发生故障时抓住问题。
For example, let’s say we want to test the path where a security group is not the default security group and nothing should be revoked. That’s everything printed in bold:
例如,假设我们要测试安全组不是默认安全组且不应撤销任何安全组的路径。 这就是所有以粗体显示的内容:
def process_event(self, event): sg_id = self._extract_sg_id(event)
if self._is_default_sg(sg_id):
self.logger.info(...)
self._revoke_and_tag(sg_id)
else: self.logger.info(...) return 'SUCCESS'[→ source]
To create a test case, let’s look at what is simple and straightforward to accomplish:
要创建一个测试用例,让我们看一下要完成的简单和直接的事情:
_extract_sg_id()
only extracts data from adict
— this can easily be called, provided that the incoming event contains the right data (taken care of by a fixture)._extract_sg_id()
仅从dict
提取数据-如果传入事件包含正确的数据(由灯具负责),则可以轻松调用该数据。_is_default_sg()
needs to call out to the EC2-API — not so easy to test, and even more importantly, not relevant for this test case. Replacing the method with a mock allows focusing on what we want to test. All we have to do is set up the mock to returnFalse
when called._is_default_sg()
需要调出EC2-API — 不太容易测试,更重要的是,与此测试用例无关。 用模拟代替方法可以专注于我们要测试的内容。 我们要做的就是将模拟程序设置为在调用时返回False
。the
else
branch is tricky, as we are testing for ‘nothing to happen’. However, we can invert the logic and make sure that theif
branch was not called. For that, we replace_revoke_and_tag()
with a mock and assert that it was not invoked. This is easy withMagicMock.assert_not_called()
(link).else
分支非常棘手,因为我们正在测试“什么都不会发生”。 但是,我们可以反转逻辑并确保未调用if
分支。 为此,我们将_revoke_and_tag()
替换为模拟并断言未调用它。 使用MagicMock.assert_not_called()
( link )很容易。
Putting everything together the test looks like this:
将所有内容放在一起测试如下所示:
def test_should_process_event_and_do_nothing_if_non_default_sg(...):
obj._is_default_sg = MagicMock(return_value=False)
obj._revoke_and_tag = MagicMock()
obj.process_event(good_event)
obj._is_default_sg.assert_called_once_with(TEST_SG)
obj._revoke_and_tag.assert_not_called()[→ source]
That’s five simple lines of code for a complete test case!
这是完整测试用例的五行简单代码!
By the way, testing the alternative path through the method (‘is default security group and should be revoked’) is almost easier, as we can assert directly on the _revoke_and_tag()
mock and don’t have to invert logic:
顺便说一句,通过该方法测试备用路径(“ 是默认安全组, 应撤销”)几乎容易_revoke_and_tag()
,因为我们可以直接在_revoke_and_tag()
模拟中进行断言,而不必反转逻辑:
def test_should_process_event_and_revoke_if_default_sg(obj, good_event):
obj._is_default_sg = MagicMock(return_value=True)
obj._revoke_and_tag = MagicMock()
obj.process_event(good_event)
obj._is_default_sg.assert_called_once_with(TEST_SG)
obj._revoke_and_tag.assert_called_once_with(TEST_SG)[→ source]
5. Use ridiculously obvious variable and parameter names
5.使用非常明显的变量和参数名称
Unlike production code, in tests it doesn’t really matter how many variables you use or how you name them. The only goal is to make tests as readable as possible.
与生产代码不同,在测试中,使用多少个变量或如何命名它们并不重要。 唯一的目标是使测试尽可能易读。
For example, we could have called the fixtures something like event1
, event2
and event3
. But how much more obvious are names like good_event
, bad_event
and unknown_event
? This test case doesn’t leave the shadow of a doubt of what’s going on:
例如,我们可以将这些灯具称为event1
, event2
和event3
。 但是,诸如good_event
, bad_event
和unknown_event
类的名字更加明显吗? 这个测试用例不会对正在发生的事情产生怀疑:
def test_should_raise_exception_if_wrong_event(obj, bad_event):
with pytest.raises(UnknownEventException):
obj._extract_sg_id(bad_event)[→ source]
Similarly, it can be a good idea to extract constants only for the purpose of giving them a name that’s self-explanatory:
同样,仅出于给它们一个不言自明的名称的目的而提取常量可能是一个好主意:
DEFAULT_GROUP = {"GroupName": "default"}NOT_A_DEFAULT_GROUP = {"GroupName": "not default"}def test_should_find_default_sg(obj):
obj.ec2_client.describe_security_groups.return_value = {"SecurityGroups": [DEFAULT_GROUP]}
assert obj._is_default_sg(TEST_SG)
def test_should_find_non_default_sg(obj):
obj.ec2_client.describe_security_groups.return_value = {"SecurityGroups": [NOT_A_DEFAULT_GROUP]}
assert not obj._is_default_sg(TEST_SG)[→ source]
Okay, one would have probably guessed the meaning of the dictionaries without the explicitly named constants. But with the constants, it’s just impossible to miss the purpose. Even more so if data structures are more complex than in this example.
好吧,如果没有显式命名的常量,可能会猜出字典的含义。 但是有了这些常数,就不可能错过目标。 如果数据结构比本示例更复杂,那就更是如此。
节省时间和精力 (Saving yourself time and effort)
Finally, let’s look at a few ways that can save you time when writing tests. This is a collection of tips that I find useful, from using the best possible tooling to focusing on the right things to test:
最后,让我们看一些可以节省编写测试时间的方法。 我收集了一些有用的技巧,从使用最好的工具到专注于要测试的正确事物:
6. Did you know about debuggers?
6.您知道调试器吗?
This is most likely a no-brainer if you have used other modern programming languages before. If not, my advice is to set yourself up with an environment that provides sophisticated support for Python.
如果您以前使用过其他现代编程语言,那么这很容易。 如果没有,我的建议是设置一个为Python提供复杂支持的环境。
A text editor will get you through hello_world(), but will leave you on your own when it comes to language-specific support like automated refactorings and — even more importantly — testing and debugging of complex code.
文本编辑器将帮助您完成hello_world()的工作 ,但是在涉及特定语言的支持(如自动重构以及(更重要的是)测试和调试复杂代码)方面 ,您将自己动手。
Combining good tests with a debugger gives you a powerful tool, where you can step through your code in an almost real-world scenario, completely in control of all aspects of the execution. Gone are the days when ‘debugging’ described the activity of gradually adding more and more print statements to the code.
将良好的测试与调试器相结合,可以为您提供一个功能强大的工具,使您可以在几乎真实的场景中逐步执行代码,从而完全控制执行的所有方面。 “调试”描述了逐渐向代码中添加越来越多的打印语句的活动的日子已经一去不复返了。
PyCharm and Visual Studio Code are both excellent IDEs that are available for free. If you can’t decide between the two, simply go with what the majority of your peers are using.
PyCharm和Visual Studio Code都是出色的IDE,可以免费获得。 如果您不能在两者之间做出选择,那就简单地选择大多数同行所使用的东西。
7. Make sure you saw every test case failing
7.确保您看到每个测试用例都失败
It might sound trivial, but it isn’t: People are making mistakes when writing code, and they also make mistakes when writing tests.
听起来很琐碎,但事实并非如此:人们在编写代码时会犯错误,而在编写测试时也会犯错误。
From my own experience, I can tell how frustrating it is to sink hours into debugging a problem, only to eventually find out that the base assumption ‘the tests are passing’ doesn’t mean that the code is doing what it’s supposed to do. Always succeeding tests are the worst kind of friends. And they are pretty easy to produce — a tiny mistake in the assertion logic could be all it takes.
从我自己的经验来看,我可以花很多时间来调试问题,最终发现基本假设“ 测试通过 ”并不意味着代码正在执行应做的事情,这是多么令人沮丧。 总是成功的测试是最糟糕的朋友。 而且它们很容易产生-断言逻辑中的微小错误可能就是全部。
A good safeguard against this is to make sure that tests are really failing when some conditions aren’t met. This can either be achieved by writing tests beforehand (also see the last tip about this), or by commenting the code that implements the feature under test.
对此的一个很好的保护措施是确保在不满足某些条件时测试确实失败。 这可以通过事先编写测试(也请参见有关此技巧的最后一个技巧),或通过注释实现被测功能的代码来实现。
8. Order test cases by relevance
8.按相关性排序测试用例
This is tiny, but valuable when looking at a screen full of tests:
这很小,但在查看充满测试的屏幕时很有价值:
It’s easier for future readers — including yourself — to get the high-level picture before getting to the details, and to read about ‘happy’ cases before diving into error scenarios.
对于未来的读者(包括您自己)来说,在深入了解细节之前更容易获得高水平的图片,并在深入研究错误方案之前先阅读有关“幸福的”案例的信息。
You don’t have to be religious about it, but it’s a real help to structure the test suite in a loose top-down style from ‘very important for how the Lambda works’ to ‘edge cases it also covers’:
您不必对此抱有虔诚的态度,但以从上至下的松散自上而下的方式构造测试套件的真正帮助,从“ 对于Lambda的工作方式非常重要 ”到“ 它还涵盖了 ”的极端 情况 :
test_should_process_event_and_revoke_if_default_sg(...)
test_should_process_event_and_do_nothing_if_non_default_sg(...)...test_should_find_default_sg(...)
test_should_not_tag_if_nothing_was_revoked(...)...test_should_raise_exception_if_unknown_event(...)
test_should_raise_exception_if_bad_event(...)
9. Hate documenting your code? — Write a test instead
9.讨厌记录您的代码? —改写测试
This is so much better than documenting code in comments or — even worse — Confluence pages: Tests are the perfect tool to describe the behaviour of your code unambiguously. And it’s pretty hard to ignore a failing test, so unlike their traditional counterparts, they don’t quietly run out of date.
这比在注释中记录代码或什至在融合页面中记录代码要好得多:测试是明确描述代码行为的理想工具。 而且很难忽略失败的测试,因此与传统的测试方法不同,测试方法不会过时。
Using tests for documentation requires two things:
使用测试记录文档需要两件事:
- An explicit test case for the aspects that matter 一个重要方面的明确测试案例
- A name that describes the intention as clearly as possible 尽可能清楚地描述意图的名称
I find the loose pattern test_should_[something that's expected]_if_[something that happened]
working well for me:
我发现松散的模式test_should_ [something that's expected] _if_ [something that happened]
对我来说运作良好:
test_should_process_event_and_revoke_if_default_sg
test_should_process_event_and_do_nothing_if_non_default_sg
test_should_raise_exception_if_wrong_event
In case of test failure, it’s already obvious what went wrong only by the name of the failing test. Want to understand what a certain Lambda does? Just read the test cases (ideally, they are ordered by relevance as per the previous tip).
在测试失败的情况下,很明显,只有失败测试的名称才可以解决问题。 是否想了解某个Lambda的功能? 只需阅读测试用例(理想情况下,它们按照上一技巧的相关性排序)。
10. Seriously, don’t test boto
10.认真,不要测试boto
(Or any other frameworks — I’m just using boto
as an example here.)
(或任何其他框架,在这里我仅以boto
为例。)
What I mean by this: When making requests to boto
, the response will usually contain a JSON object that requires filtering to get the relevant data out:
我的意思是:向boto
发出请求时,响应通常会包含一个JSON对象,该对象需要过滤才能获取相关数据:
{
"SecurityGroups": [
{
"Description": "default VPC security group",
"GroupName": "default",
"IpPermissions": [],
"OwnerId": "123456789012",
"GroupId": "sg-12345678",
"IpPermissionsEgress": [],
"Tags": [],
"VpcId": "vpc-12345678"
}
],
"ResponseMetadata": {
"RequestId": "12345678-3233-4eff-969f-7c6b43ff8f60",
"HTTPStatusCode": 200,
"HTTPHeaders": {
"x-amzn-requestid": "12345678-3133-4eff-969f-7c6b43ff8f60",
"content-type": "text/xml;charset=UTF-8",
"content-length": "857",
"date": "Sat, 02 May 2020 12:34:56 GMT",
"server": "AmazonEC2"
},
"RetryAttempts": 0
}
}
Writing tests that cover parsing of dictionaries can be tempting — but chances are this is adding limited value. They mostly create noise and make it harder to focus on the important bits. Also, if boto
ever changes the format of their response there’s nothing we can do about it in the first place — we’ll most likely end up adopting our code to their changes. So there’s no point in covering boto
’s requests or responses in our tests!
编写涵盖字典解析的测试可能会很诱人-但这可能会增加有限的价值。 它们大多会产生噪音,使人们更难以集中精力于重要部位。 另外,如果boto
改变了他们的回应格式,那么我们一开始就无能为力-我们很可能最终会采用我们的代码来改变他们。 因此,在我们的测试中覆盖boto
的请求或响应毫无意义!
I find a good pattern is isolating boto
call and filtering operation to a single method:
我发现一个很好的模式是将boto
调用和过滤操作隔离为一个方法:
def _is_default_sg(self, sg_id):
sec_groups = self.ec2_client.describe_security_groups(GroupIds=[sg_id])['SecurityGroups']
return sec_groups[0]['GroupName'] == 'default'[→ source]
For unit tests, all that’s required is setting up the expectation for the complete method:
对于单元测试,所需要做的就是设置对完整方法的期望:
obj._is_default_sg = MagicMock(return_value=True)[→ source]
That’s a single line of test code to set up even complex invocations — whilst being completely decoupled from the actual interaction with the framework.
这是测试代码的单行代码,用于设置甚至复杂的调用-同时与与框架的实际交互完全脱钩。
11. Don’t look too hard at the test coverage
11.不要太看测试范围
With pytest
and pytest-cov
, it’s very easy to measure test coverage — simply add some arguments to the pytest
invocation and produce a coverage report:
使用pytest
和pytest-cov
,很容易测量测试覆盖率-只需在pytest
调用中添加一些参数并生成覆盖率报告:
pytest --cov=src --cov-branch --cov-report term-missing[...]Name Stmts Miss Branch BrPart Cover
--------------------------------------------------------------------
src/revokedefaultsg/app.py 59 11 12 1 83% --------------------------------------------------------------------TOTAL 59 11 12 1 83%
While it does make sense to look at test coverage to identify untested code, don’t get too hung up the metric itself.
尽管查看测试覆盖范围以识别未经测试的代码确实很有意义,但不要过于依赖该指标本身。
First of all, aiming for 100% — or near 100% — test coverage is not realistic; and closing the last 20% gap will cost a lot of extra effort. Second, it’s far more important to focus on the important bits and readability than on keeping test coverage above a certain threshold.
首先,瞄准100%或接近100%的测试覆盖率是不现实的; 而缩小最后20%的差距将花费大量的额外精力。 其次,将重点放在重要的位和可读性上比将测试覆盖范围保持在一定阈值上更为重要。
I remember writing tests for Java getters and setters only to keep the test coverage high and the numbers looking good on paper. What a waste of time and energy in hindsight.
我记得为Java getter和setter编写测试只是为了保持很高的测试覆盖率,并且数字看起来很漂亮。 事后看来,这是浪费时间和精力。
Similarly, great test coverage doesn’t necessarily imply great tests. I’ve seen 90% coverage achieved by a single test case, comparing 1,000 lines of input with 10,000 lines of output. While the result looked impressive on paper, the failing test created a single assertion failure that’s 20,000 lines long. Impossible to understand without installing a whole range of diff tools on your computer.
同样,良好的测试覆盖率不一定意味着良好的测试。 我已经看到一个测试用例实现了90%的覆盖率,将1000行输入与10,000行输出进行了比较。 尽管结果在纸面上看起来令人印象深刻,但失败的测试创建了一个长达20,000行的断言失败。 在计算机上未安装所有差异工具的情况下无法理解。
My recommendation: Write tests around features, not around coverage.
我的建议:围绕功能而不是覆盖范围编写测试。
12. Consider giving test-driven development a try
12.考虑尝试一下测试驱动的开发
As a last tip, I want to ask you to look at when you are writing tests. Because implementing unit tests doesn’t have to be the last step in the process.
最后一点,我想请您在编写测试时查看一下。 因为实现单元测试不一定是过程的最后一步。
When thinking about a new feature, it’s perfectly feasible to write a new test case first, see it failing and then implement the code to make the test pass. When fixing a bug, why not starting with a test that surfaces the problem before fixing the code itself?
考虑新功能时,首先编写一个新的测试用例,看看它是否失败,然后实施代码以使测试通过是完全可行的。 修复错误时,为什么不先进行测试以发现问题,然后再修复代码本身?
I can almost guarantee that your developer experience will be more rewarding and that oftentimes the quality of the outcome will be higher.
我几乎可以保证,您的开发人员经验会带来更多回报,并且通常结果质量会更高。
This is because tests allow you to change perspective: They put you in the shoes of the consumer and make you look at your code from a feature perspective.
这是因为测试使您可以改变视角:它们使您不知所措,并使您从功能的角度看待代码。
Also, they give you the peace of mind that your code does what it’s supposed to do. This makes it much easier to continuously refactor to cater for new features — as opposed to ‘surgically inject’ more and more functionality into a code base that has become too complex to mess with.
而且,它们使您放心,您的代码可以执行预期的工作。 这使得连续重构以适应新功能变得容易得多,这与将“越来越多”的功能“外科手术”注入到过于复杂以致无法处理的代码库相对。
And last but not least it’s pretty hard to make the same mistake twice if you already have a test case that proves the code is working.
最后但并非最不重要的一点是,如果您已经有一个测试用例证明了代码可以正常工作,那么很难两次犯同样的错误。
恢复 (Resume)
Many of the tips and recommendation only scratch the surface of much deeper stories that go well beyond the scope of writing a single Lambda. If something has spawned your interest, I would like to encourage you to keep on reading about the topic. If I had to recommend a single book on writing better code, it would be “The Pragmatic Programmer”, by Andrew Hunt and David Thomas.
许多技巧和建议仅触及了更深层故事的表面,这些故事远超出编写单个Lambda的范围。 如果有什么引起您的兴趣,我想鼓励您继续阅读该主题。 如果我不得不推荐一本关于编写更好的代码的书,那本书将是安德鲁·亨特和戴维·托马斯的“ 实用程序员 ”。
In the article, I made the call to assume Python knowledge as a baseline and am not explaining any language features I’m using. This would have taken the focus away from the main topic and would have made it harder to follow along. However, I tried my best to make the code as readable as possible.
在本文中,我呼吁将Python知识作为基线,而不是在解释我使用的任何语言功能。 这本来可以将重点从主要话题上移开,并且会使后续工作变得更加困难。 但是,我尽力使代码尽可能可读。
Also, a bit of a disclaimer about the example code I’m using: It’s a challenge to strike the balance between something simple enough to fit into the format of this article, but complex enough to illustrate problems well. If some recommendations seem like overkill for a Lambda with less than 100 lines of code, the picture looks completely different for bigger Lambdas in a more complex context.
另外,对于我正在使用的示例代码有些免责声明:在足够简单的东西(适合本文的格式)和足够复杂的问题(要很好地说明问题)之间取得平衡是一个挑战。 如果某些建议对于使用少于100行代码的Lambda来说似乎是过大的,那么在更大的上下文中,对于较大的Lambda来说,图片看起来就完全不同了。
And last but not least I want you to take away that one message behind all this: Writing good code and good tests is a lot easier than people might think.
最后但并非最不重要的一点是,我希望您能消除所有这些背后的信息: 编写好的代码和好的测试比人们想象的要容易得多 。
Please leave a comment if you have a question or want to share your thoughts!
如果您有任何疑问或想分享您的想法,请发表评论!
Happy Coding :)
快乐编码:)