单元测试工具包

twin-sister的Python项目详细描述


Twin Sister:

纯python依赖注入的单元测试工具包

No, I am Zoot's identical twin sister, Dingo.

孪生姐妹如何帮助您

不管你是否接受迈克尔·费瑟对“遗留代码”的定义 “没有测试的代码”,你知道你应该编写单元测试 如果这些测试足够清晰,可以显示您的代码 足够有效地告诉你什么时候你弄坏了什么。上 另一方面,编写好的单元测试可能是hard--特别是在它们需要的时候 以涵盖单元与外部组件的交互。

输入孪生姐妹。最初是2016年ProtecWise的一个内部项目, 在2017年作为开源发布,并一直在不断扩展 从那以后就用。它的目标是使单元测试更易于编写和 阅读时不要对测试系统使用暴力。它由一个 测试双倍库和一个纯python依赖注入程序来交付它们 (或者其他适合你的东西)。

实际情况如何

test_post_something.py

from unittest import TestCase

from expects import expect, equal
import requests
from twin_sister import open_dependency_context
from twin_sister.fakes import EmptyFake, FunctionSpy

from post_something import post_something

class TestPostSomething(TestCase):

  def setUp(self):
      self.context = open_dependency_context()
      self.post_spy = FunctionSpy()
      requests_stub = EmptyFake(pattern_obj=requests)
      requests_stub.post = self.post_spy
      self.context.inject(requests, requests_stub)

  def tearDown(self):
      self.context.close()

  def test_uses_post_method(self):
      post_something('yadda')
      self.post_spy.assert_was_called()

  def test_sends_specified_content(self):
      content = 'yadda yadda yadda'
      post_something(content)
      expect(self.post_spy['data']).to(equal(content))  

post_something.py

import requests
from twin_sister import dependency

def post_something(content):
    post = dependency(requests).post
    post('http://example.com/some-api', data=content)

了解更多

Dependency injection mechanism

What is dependency injection and why should I care?

If you write tests for non-trivial units, you have encountered situations where the unit you are testing depends on some component outside of itself. For example, a unit that retrieves data from an HTTP API depends on an HTTP client. By definition, a unit test does not include systems outside the unit, so does not make real network requests. Instead, it configures the unit to make fake requests using a component with the same interface as the real HTTP client. The mechanism that replaces the real HTTP client with a fake one is a kind of dependency injection.

Dependency injection techniques

Most simple: specify initializer arguments

^{pr 3}$

In the example above, new knight objects will ordinarily construct a real HTTP client for themselves, but the code that creates them has the opportunity to inject an alternative client like this:

^{pr 4}$

This approach has the advantage of being simple and straightforward and can be more than adequate if the problem space is small and well-contained. It begins to break down, however, as the system under test becomes more complex. One manifestation of this breakdown is the appearance of "hobo arguments." The initializer must specify each dependency that can be injected and the target bears responsibility for maintaining each injected object and passing it to sub-components as they are created.

For example

^{pr 5}$

^{} is a hobo. The only reason ^{} has for accepting it is to pass it through to ^{}. This is awkward, aside from its damage to separation of concerns.

Most thorough: subvert the global symbol table

In theory, it would be possible to make all HTTP clients fake by redirecting HttpClient in the global symbol table to FakeHttpClient. This approach has the advantage of not requiring the targeted code to be aware of the injection and is likely to be highly effective if successful. It suffers from major drawbacks, however. The symtable module (sensibly) does not permit write access, so redirection would need to be performed at a lower level which would break compatibility across Python implementations. It's also an extreme hack with potentially serious side effects.

Middle ground: request dependencies explicitly

Twin Sister takes a middle approach. It maintains a registry of symbols that have been injected and then handles requests for dependencies. In this way, only code that requests a dependency explicity is affected by injection:

^{pr 6}$

^{} returns the injected replacement if one exists. Otherwise, it returns the real thing. In this way, the system will behave sensibly whether injection has occurred or not.

Injecting a dependency with Twin Sister

Installation from pip

^{pr 7}$

Installation from source

^{pr 8}$

Generic technique to inject any object

^{pr 9}$

Injection is effective only inside the dependency context. Inside the context, requests for ^{} will return ^{}. Outside the context (after the ^{} statement), requests for ^{} will return ^{}.

Injecting a class that always produces the same object

^{pr 10}$

Each time the system under test executes code like this

^{pr 11}$

fresh_horse will be the same old eric_the_horse.

Support for xUnit test pattern

Instead of using a context manager, a test can open and close its dependency context explicitly:

^{pr 12}$

Support for multi-threaded tests

By default, Twin Sister maintains a separate dependency context for each thread. This allows test cases with different dependency schemes to run in parallel without affecting each other.

However, it also provides a mechanism to attach a dependency context to a running thread:

^{pr 13}$

The usual rules about context scope apply. Even if the thread continues to run, the context will disappear after the ^{} statement ends.

The dependency context and built-in fakery

The dependency context is essentially a dictionary that maps real objects to their injected fakes, but it also knows how to fake some commonly-used components from the Python standard library.

Fake environment variables

Most of the time, we don't want our unit tests to inherit real environment variables because that would introduce an implicit dependency on system configuration. Instead, we create a dependency context with ^{}. This creates a fake set of environment variables, initially empty. We can then add environment variables as expected by our system under test:

^{pr 14}$

The fake environment is just a dictionary in an injected ^{}, so the system-under-test must request it explicitly as a dependency:

^{pr 15}$

The injected ^{} is mostly a passthrough to the real thing.

Fake logging

Most of the time, we don't want our unit tests to use the real Python logging system -- especially if it writes messages to standard output (as it usually does). This makes tests fill standard output with noise from useless logging messages. Some of the time, we want our tests to see the log messages produced by the system-under-test. The fake log system meets both needs.

^{pr 16}$

Fake filesystem

Most of the time, we don't want our unit tests to use the real filesystem. That would introduce an implicit dependency on actual system state and potentially leave a mess behind. To solve this problem, the dependency context can leverage pyfakefs提供一个假文件系统。

with dependency_context(supply_fs=True):
  filename = 'favorites.txt'
  open = dependency(open)
  with open(filename, 'w') as f:
     f.write('some of my favorite things')
  with open(filename, 'r') as f:
     print('From the fake file: %s' % f.read())
  assert dependency(os).path.exists(filename)
assert not os.path.exists(filename)

Fake time

Sometimes it is useful -- or even necessary -- for a test case to control time as its perceived by the system-under-test. The classic example is a routine that times out after a specified duration has elapsed. Thorough testing should cover both sides of the boundary, but it is usually undesirable or impractical to wait for the duration to elapse. That is where TimeController comes in. It's a self-contained way to inject a fake datetime.datetime:

^{pr 18}$

The example above checks for the presence or absence of an exception, but it is possible to check any state. For example, let's check the impact of a long-running bound method on its object:

^{pr 19}$

We can also check the return value of the target function:

^{pr 20}$

By default, TimeController has its own dependency context, but it can inherit a specified one instead:

^{pr 21}$

There are limitations. The fake datetime affects only .now() and .utcnow() at present. This may change in a future release as needs arise.

Test Doubles

Classically, test doubles fall into three general categories:

Stubs

A stub faces the unit-under-test and mimics the behavior of some external component.

Spies

A spy faces the test and reports on the behavior of the unit-under-test.

Mocks

A mock is a stub that contains assertions. Twin Sister's ^{} module has none of these but most of the supplied fakes are so generic that mock behavior can be added.

Supplied Stubs

MutableObject

Embarrassingly simple, but frequently useful for creating stubs on the fly:

^{pr 22}$

EmptyFake

An extremely generic stub that aims to be a substitute for absolutely anything. Its attributes are EmptyFake objects. When it's called like a function, it returns another EmptyFake.

When invoked with no arguments, EmptyFake creates the most flexible fake possible:

^{pr 23}$

It's possible to restrict an EmptyFake to attributes defined by some other object:

^{pr 24}$

It's also possible to restrict an EmptyFake to attributes declared by a class:

^{pr 25}$

Important limitation: "declared by a class" means that the attribute appears in the class declaration. If the attribute gets created by the initializer instead, then it's not declared by the class and EmptyFake will insist that the attribute does not exist. If you need an attribute that gets created by the initializer, you're better off instantiating an object to use as a ^{}.

empty_context_manager

A context manager that does nothing and yields an EmptyFake, useful for preventing unwanted behavior like opening network connections.

^{pr 26}$

A generic EmptyFake object will also serve as a context manager without complaints.

FakeDateTime

A datetime.datetime stub that reports that reports a fixed time.

^{pr 27}$

Supplied Spies

FunctionSpy

Pretends to be a real function and tracks calls to itself.

^{pr 28}$

MasterSpy

The spy equivalent of EmptyFake, MasterSpy tracks every interaction and spawns more spies to track interactions with its attributes.

^{pr 29}$

By default MasterSpy spawns spies only for attributes that are functions.

Expects Matchers

Custom matchers for expects,一个 另一种断言方式。

投诉

expects.raise_error如果引发意外异常,则会悄悄返回false。 twin_sister.expects_matchers.complain相反,将重新引发异常。 否则,匹配者本质上是等价的。

from expects import expect, raise_error
from twin_sister.expects_matchers import complain

class SpamException(RuntimeError):
  pass

class EggsException(RuntimeError):
   pass

def raise_spam():
   raise SpamException()

def raise_eggs():
   raise EggsException()

# both exit quietly because the expectation is met
expect(raise_spam).to(raise_error(SpamException))
expect(raise_spam).to(complain(SpamException))

# exits quietly because a different exception was raised
expect(raise_eggs).not_to(raise_error(SpamException))

# re-raises the exception because it differs from the expectation
expect(raise_eggs).not_to(complain(SpamException))

包含所有项目

如果一个字典包含另一个字典中的所有项,则返回true。

from expects import expect
from twin_sister.expects_matchers import contain_all_items_in

expect({'foo': 1, 'bar': 2}).to(contain_all_items_in({'foo': 1}))
expect({'foo': 1}).not_to(contain_all_items_in({'foo': 1, 'bar': 2}))

欢迎加入QQ群-->: 979659372 Python中文网_新手群

推荐PyPI第三方库


热门话题
ByteArrayOutputStream的java解码属性   java S3 SDK在上载时更新单个对象,而不是创建新文件   java hibernate:无法从eclipse连接到DB   java如何在强制转换JComboBox之前检查其类型?   http从Java中的GETPOST请求方法捕获URI、资源名称,如开发人员工具中所示   java在Spring@Bean方法中返回接口的局限性   Java中的Web服务和客户端(使用Eclipse Apache Axis 2自底向上服务)某些代码会引发异常   java spring安全+rest不起作用   java将LinkedList添加到包含LinkedList的LinkedList并更改添加的LinkedList   java是否临时删除对象的属性?   java使用AnimatedGifEncoder类创建的gif图像的部分帧是不透明的   java如何高效地处理maven3时间戳快照?   java向集合对象添加另一项   java如何将动态参数传递给jquery函数   java使用libGdx桌面端口作为Android GLES20的仿真器