Introducing time-machine, a new Python library for mocking the current time

栏目: IT技术 · 发布时间: 4年前

内容简介:Whilst writingThis post is my introduction to the problem, the trade-offs with the other libraries, and how my new library,It’s especially common in web projects to have features that rely on changes to the current date and time. For example, you might hav
Introducing time-machine, a new Python library for mocking the current time

Whilst writing Speed Up Your Django Tests , I wanted to add a section about mocking the current time. I knew of two libraries such mocking, but I found it hard to pick one to recommend due to the trade-offs in each. So I delayed adding that section and shaved a rather large yak by writing a third library.

This post is my introduction to the problem, the trade-offs with the other libraries, and how my new library, time-machine , tries to solve them. The next version of Speed Up Your Django Tests can now include a section on mocking time.

The Problem

It’s especially common in web projects to have features that rely on changes to the current date and time. For example, you might have a feature where each customer may only make an order once per day.

For testing such features, it’s often necessary to mock the functions that return the current date and time. In Python’s standard library, these live in the datetime and time modules.

There are various ways of doing this mocking: it can be done generically with unittest.mock , or in a more targeted fashion with a time-mocking library.

Using unittest.mock works, but it’s often inaccurate each patcher can only mock a single function reference. This inaccuracy is exacerbated when mocking the current time, as there are many different functions that return the current time, in different formats. Due to the way Python’s imports work, it takes a lot of mocks to replace every instance of functions from datetime and time in a code path, and is sometimes impossible. (See: Why Your Mock Doesn’t Work .)

I know of two existing Python libraries that have tried to provide a better way to mock the current time: freezegun and libfaketime . Let’s look at them now, before I introduce time-machine .

freezegun

freezegun is a very popular library for mocking the current time. It has a great, clear API, and “does what it says on the tin.”

For example, you can write a time-mocking test like so:

import datetime as dt
import freezegun

@freezegun.freeze_time("1955-11-05 01:22")
def test_delorean():
    assert dt.date.today().isoformat() == "1955-11-05"

The main drawback is its slow implementation. It essentially does a find-and-replace mock of all the places that the relevant functions from the datetime and time modules have been imported. This gets around the problems with using unittest.mock , but it means the time it takes to do the mocking is proportional to the number of loaded modules. In large projects, this can take a second or two, an impractical overhead for each individual test.

It’s also not a perfect search, since it searches only module-level imports. Such imports are definitely the most common way projects use date and time functions, but they’re not the only way. freezegun won’t find functions that have been “hidden” inside arbitrary objects, such as class-level attributes.

It also can’t affect C extensions that call the standard library functions, including (I believe) Cython-ized Python code.

libfaketime

python-libfaketime is a much less popular library, but it is much faster. It wraps the LD_PRELOAD library libfaketime, which replaces all the C-level system calls for the current time with its own wrappers. It’s therefore a “perfect” mock, affecting every single point the current time might be fetched from the current process.

It also has much the same API:

import datetime as dt
import libfaketime

@libfaketime.fake_time("1955-11-05 01:22")
def test_delorean():
    assert dt.date.today().isoformat() == "1955-11-05"

The approach is much faster since starting the mock only requires changing an environment variable that libfaketime reads. The python-libfaketime README has a benchmark showing it working 300 times faster than freezegun. This benchmark even puts freezegun at an advantage, since it doesn’t import any extra dependencies, and freezegun’s runtime is proportional to the number of imported modules.

I learnt about python-libfaketime at YPlan, where we were first using freezegun. Moving to python-libfaketime took our Django project’s test suite (of several thousand tests) from 5 minutes to 3 minutes.

Unfortunately python-libfaketime comes with the limitations of LD_PRELOAD . This is a mechanism to replace system libraries for a program as it loads ( explanation ). This causes two issues in particular when you use python-libfaketime.

First, LD_PRELOAD is only available on Unix platforms, which prevents you from using it on Windows. This can be a blocker for many teams.

Second, you have to help manage LD_PRELOAD . You either use python-libfaketime’s reexec_if_needed() function, which restarts (re-execs) your test process while loading, or manually manage the LD_PRELOAD environment variable. Neither is ideal. Re-execing breaks anything that might wrap your test process, such as profilers, debuggers, and IDE test runners. Manually managing the environment variable is a bit of overhead, and must be done for each environment you run your tests in, including each developer’s machine.

time-machine

My new library, time-machine , is intended to combine the advantages of freezegun and libfaketime. It works without LD_PRELOAD but still mocks the standard library functions everywhere they may be referenced. It does so by modifying the built-in functions at the C level, to point them through wrappers that return different values when mocking. Normally in Python, built-in functions are immutable, but time-machine overcomes this by using C code to replace their function pointers.

Again, it has much the same API as freezegun, except from the names:

import datetime as dt
import time_machine

@time_machine.travel("1955-11-05 01:22")
def test_delorean():
    assert dt.date.today().isoformat() == "1955-11-05"

Its weak point is that libraries making their own system calls won’t be mocked. (Cython use of the datetime and time modules should be mocked, although I haven’t tested it yet). However I believe such usage is rare in Python programs - freezegun also shares this weakness, but that hasn’t stopped it becoming popular.

If you have time, please try out time-machine in your tests! It’s available now for Python 3.6+. Because of its implementation, it only works with CPython, not PyPy or any other interpreters.

It’s my first open source project using a C extension. Let me know how it works, and if you’re switching from freezegun, how much it speeds up your tests. If it is found to work well, it may be possible to merge its technique into freezegun, to share the speed boost without causing churn.

Fin

May time make you ever stronger,

—Adam

Are your Django project's tests slow?Read Speed Up Your Django Tests now!


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

用数据讲故事

用数据讲故事

[美] Cole Nussbaumer Knaflic / 陆 昊、吴梦颖 / 人民邮电出版社 / 2017-8 / 59.00元

本书通过大量案例研究介绍数据可视化的基础知识,以及如何利用数据创造出吸引人的、信息量大的、有说服力的故事,进而达到有效沟通的目的。具体内容包括:如何充分理解上下文,如何选择合适的图表,如何消除杂乱,如何聚焦受众的视线,如何像设计师一样思考,以及如何用数据讲故事。一起来看看 《用数据讲故事》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具