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!


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

查看所有标签

猜你喜欢:

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

着陆页:获取网络订单的关键

着陆页:获取网络订单的关键

谢松杰 / 电子工业出版社 / 2017-1-1 / CNY 55.00

着陆页是用户点击广告后看到的第一个页面,是相关产品和服务的商业模式与营销思想的载体,是实现客户转化的关键。本书从“宏观”和“微观”两个层面对着陆页的整体框架和局部细节进行了深入的讨论,既有理论和方法,又有技术与工具,为读者呈现了着陆页从策划到技术实现的完整知识体系,帮助读者用最低的成本实现网站最高的收益。 谢松杰老师作品《网站说服力》版权输出台湾,深受两岸读者喜爱。本书是《网站说服力》的姊妹......一起来看看 《着陆页:获取网络订单的关键》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

URL 编码/解码
URL 编码/解码

URL 编码/解码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换