2016年8月

这篇教程说明你应该怎样配置 nginx、设置 HTTP 头部过期时间,用 Cache-Control 中的 max-age 标记为静态文件(比如图片、 CSS 和 Javascript 文件)设置一个时间,这样用户的浏览器就会缓存这些文件。这样能节省带宽,并且在访问你的网站时会显得更快些(如果用户第二次访问你的网站,将会使用浏览器缓存中的静态文件)。

1、准备事项

我想你需要一个正常工作的 nginx 软件,就像这篇教程里展示的:在 Ubuntu 16.04 LTS 上安装 Nginx,PHP 7 和 MySQL 5.7 (LEMP)

2 配置 nginx

可以参考 expires 指令手册来设置 HTTP 头部过期时间,这个标记可以放在 http {}server {}location {} 等语句块或者 location {} 语句块中的条件语句中。一般会在 location 语句块中用 expires 指令控制你的静态文件,就像下面一样:

location ~*  \.(jpg|jpeg|png|gif|ico|css|js)$ {
   expires 365d;
}

在上面的例子中,所有后缀名是 .jpg.jpeg.png.gif.ico.css.js 的文件会在浏览器访问该文件之后的 365 天后过期。因此你要确保 location {} 语句块仅仅包含能被浏览器缓存的静态文件。

然后重启 nginx 进程:

/etc/init.d/nginx reload

你可以在 expires 指令中使用以下的时间设置:

  • offExpiresCache-Control 头部不能被更改。
  • epochExpires 头部设置成 1970 年 1 月 1 日 00:00:01。
  • max 设置 Expires 头部为 2037 年 12 月 31 日 23:59:59,设置 Cache-Control 的最大存活时间为 10 年
  • 没有 @ 前缀的时间意味着这是一个与浏览器访问时间有关的过期时间。可以指定一个负值的时间,就会把 Cache-Control 头部设置成 no-cache。例如:expires 10d 或者 expires 14w3d
  • @ 前缀的时间指定在一天中的某个时间过期,格式是 Hh 或者 Hh:Mm,H 的范围是 0 到 24,M 的范围是 0 到 59,例如:expires @15:34

你可以用以下的时间单位:

  • ms: 毫秒
  • s: 秒
  • m: 分钟
  • h: 小时
  • d: 天
  • w: 星期
  • M: 月 (30 天)
  • y: 年 (365 天)

例如:1h30m 表示一小时三十分钟,1y6M 表示一年六个月。

注意,要是你用一个在将来很久才会过期的头部,当组件修改时你就要改变组件的文件名。因此给文件指定版本是一个不错的方法。例如,如果你有个 javascript.js 文件 并且你要修改它,你可以在修改的文件名字后面添加一个版本号。这样浏览器就要下载这个文件,如果你没有更改文件名,浏览器将从缓存里面加载(旧的)文件。

除了把基于浏览器访问时间设置 Expires 头部(比如 expires 10d)之外,也可以通过在时间前面的 modified 关键字,将 Expires 头部的基准设为文件修改的时间(请注意这仅仅对存储在硬盘的实际文件有效)。

expires modified 10d;

3 测试

要测试你的配置是否有效,可以用火狐浏览器的开发者工具中的网络分析功能,然后用火狐访问一个静态文件(比如一张图片)。在输出的头部信息里,应该能看到 Expires 头部和有 max-age 标记的 Cache-Control 头部(max-age 标记包含了一个以秒为单位的值,比如 31536000 就是指今后的一年)

4 链接

nginx 的 Http 头部模块(HttpHeadersModule): http://wiki.nginx.org/HttpHeadersModule


via: https://www.howtoforge.com/tutorial/how-to-cache-static-files-on-nginx/

作者:Falko Timme 译者:GitFuture 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

当你回顾所有到目前为止我们已经覆盖的 awk 实例,从 awk 系列的开始,你会注意到各种实例的所有指令是顺序执行的,即一个接一个地执行。但在某些情况下,我们可能希望基于一些条件进行文本过滤操作,即流程控制语句允许的那些语句。

在 awk 编程中有各种各样的流程控制语句,其中包括:

  • if-else 语句
  • for 语句
  • while 语句
  • do-while 语句
  • break 语句
  • continue 语句
  • next 语句
  • nextfile 语句
  • exit 语句

然而,对于本系列的这一部分,我们将阐述:if-elseforwhiledo while 语句。请记住,我们已经在这个 awk 系列的第 6 部分介绍过如何使用 awk 的 next 语句。

1. if-else 语句

如你想的那样。if 语句的语法类似于 shell 中的 if 语句:

if  (条件 1) {
     动作 1
}
else {
      动作 2
}

在上述语法中,条件 1条件 2 是 awk 表达式,而动作 1动作 2 是当各自的条件得到满足时所执行的 awk 命令。

条件 1 满足时,意味着它为真,那么动作 1 被执行并退出 if 语句,否则动作 2 被执行。

if 语句还能扩展为如下的 if-else_if-else 语句:

if (条件 1){
     动作 1
}
else if (条件 2){
      动作 2
}
else{
     动作 3
}

对于上面的形式,如果条件 1 为真,那么动作 1 被执行并退出 if 语句,否则条件 2 被求值且如果值为真,那么动作 2 被执行并退出 if 语句。然而,当条件 2 为假时,那么动作 3 被执行并退出 if 语句

这是在使用 if 语句的一个实例,我们有一个用户和他们年龄的列表,存储在文件 users.txt 中。

我们要打印一个清单,显示用户的名称和用户的年龄是否小于或超过 25 岁。

aaronkilik@tecMint ~ $ cat users.txt
Sarah L         35      F
Aaron Kili      40      M
John  Doo       20      M
Kili  Seth      49      M

我们可以写一个简短的 shell 脚本来执行上文中我们的工作,这是脚本的内容:

#!/bin/bash
awk ' {
        if ( $3 <= 25 ){
           print "User",$1,$2,"is less than 25 years old." ;
        }
        else {
           print "User",$1,$2,"is more than 25 years old" ;
        }
}'    ~/users.txt

然后保存文件并退出,按如下方式使脚本可执行并运行它:

$ chmod +x test.sh
$ ./test.sh

输出样例

User Sarah L is more than 25 years old
User Aaron Kili is more than 25 years old
User John Doo is less than 25 years old.
User Kili Seth is more than 25 years old

2. for 语句

如果你想在一个循环中执行一些 awk 命令,那么 for 语句为你提供一个做这个的合适方式,格式如下:

for ( 计数器的初始化 ; 测试条件 ; 计数器增加 ){
      动作
}

这里,该方法是通过一个计数器来控制循环执行来定义的,首先你需要初始化这个计数器,然后针对测试条件运行它,如果它为真,执行这些动作并最终增加这个计数器。当计数器不满足条件时,循环终止。

在我们想要打印数字 0 到 10 时,以下 awk 命令显示 for 语句是如何工作的:

$ awk 'BEGIN{ for(counter=0;counter<=10;counter++){ print counter} }'

输出样例

0
1
2
3
4
5
6
7
8
9
10

3. while 语句

while 语句的传统语法如下:

while ( 条件 ) {
          动作
}

这个条件是一个 awk 表达式而动作是当条件为真时被执行的 awk 命令。

下面是一个说明使用 while 语句来打印数字 0 到 10 的脚本:

#!/bin/bash
awk ' BEGIN{ counter=0;
        while(counter<=10){
              print counter;
              counter+=1;
        }
}'

保存文件并使脚本可执行,然后运行它:

$ chmod +x test.sh
$ ./test.sh

输出样例

0
1
2
3
4
5
6
7
8
9
10

4. do while 语句

它是上文中 while 语句的一个变型,具有以下语法:

do {
     动作
}
 while (条件)

这轻微的区别在于,在 do while 语句下,awk 的命令在求值条件之前执行。使用上文 while 语句的例子,我们可以通过按如下所述修改 test.sh 脚本中的 awk 命令来说明 do while 语句的用法:

#!/bin/bash

awk ' BEGIN{ counter=0;
        do{
            print counter;
            counter+=1;
        }
        while (counter<=10)
}'

修改脚本之后,保存文件并退出。按如下方式使脚本可执行并执行它:

$ chmod +x test.sh
$ ./test.sh

输出样例

0
1
2
3
4
5
6
7
8
9
10

总结

这不是关于 awk 的流程控制语句的一个全面的指南,正如我早先提到的,在 awk 里还有其他几个流程控制语句。

尽管如此,awk 系列的这一部分使应该你明白了一个明确的基于某些条件控制的 awk 命令是如何执行的基本概念。

你还可以了解其余更多的流程控制语句以获得更多关于该主题的理解。最后,在 awk 的系列下一节,我们将进入编写 awk 脚本。


via: http://www.tecmint.com/use-flow-control-statements-with-awk-command/ 作者:Aaron Kili 译者:robot527 校对:wxy

本文由 LCTT 原创编译,Linux 中国 荣誉推出

今日关注

基于 Red Hat Enterprise Linux 6 的 CentOS 6 内核进行了安全更新。事实上,此次内核的更新涉及到了 Red Hat Enterprise Linux Desktop 6, Red Hat Enterprise Linux Workstation 6, Red Hat Enterprise Linux Server 6, 和 Red Hat Enterprise Linux HPC Node 6 的相关产品,其中一项非常重要的安全问题就是 Linux 内核网络子系统的一个漏洞,离线攻击者可以利用该漏洞获取到特定网络连接的相关信息。此次内核更新还修复了另外7个 bug。使用上面提到的操作系统的用户需要赶紧进行内核的更新了。

图文摘要

Calamares 2.4 发布。这是一款 GUN/Linux 发行版本中默认使用的通用安装程序框架。这一版本增加了 netinstall 模块,允许安装过程中同时安装多个包,使用挂载的轮询设备作为 unpackfs 操作源,优化了多个配置文件,改进了逻辑分区的颜色显示,并解决了用户名验证的一个 bug。现在已经可以下载使用了。

KDevelop 5.0 IDE 正式发布。这一版本提供了对C/C++语言更好的支持,采用了 LLVM/Clang 编译器,代替了传统的 C++ 分析引擎。同时,成功迁移到了 KDE Frameworks 5 和 Qt 5 。还有更多新特性,赶紧下载体验吧。

GNU/Linux 发行版中默认的网络管理器 NetworkManager 1.4 发布。这一版本中,可以通过使用 'ipv6.token' 来对 IPv6 令牌化的接口标识符进行设置,支持 oFono 作为 modem 管理器,并支持在系统挂掉之前自动断开网络连接。还有很多新的功能,感兴趣的用户赶紧进行下载体验吧。

为了解决标准的“用户-组-其他/读-写-执行”权限以及访问控制列表的限制以及加强安全机制,美国国家安全局(NSA)设计出一个灵活的 强制访问控制 Mandatory Access Control (MAC)方法 SELinux(Security Enhanced Linux 的缩写),来限制标准的权限之外的种种权限,在仍然允许对这个控制模型后续修改的情况下,让进程尽可能以最小权限访问或在系统对象(如文件,文件夹,网络端口等)上执行其他操作。

SELinux 和 AppArmor 加固 Linux 安全

另一个流行并且被广泛使用的 MAC 是 AppArmor,相比于 SELinux 它提供更多的特性,包括一个学习模式,可以让系统“学习”一个特定应用的行为,以及通过配置文件设置限制实现安全的应用使用。

在 CentOS 7 中,SELinux 合并进了内核并且默认启用 强制 Enforcing 模式(下一节会介绍这方面更多的内容),与之不同的是,openSUSE 和 Ubuntu 使用的是 AppArmor 。

在这篇文章中我们会解释 SELinux 和 AppArmor 的本质,以及如何在你选择的发行版上使用这两个工具之一并从中获益。

SELinux 介绍以及如何在 CentOS 7 中使用

Security Enhanced Linux 可以以两种不同模式运行:

  • 强制 Enforcing :这种情况下,SELinux 基于 SELinux 策略规则拒绝访问,策略规则是一套控制安全引擎的规则。
  • 宽容 Permissive :这种情况下,SELinux 不拒绝访问,但如果在强制模式下会被拒绝的操作会被记录下来。

SELinux 也能被禁用。尽管这不是它的一个操作模式,不过也是一种选择。但学习如何使用这个工具强过只是忽略它。时刻牢记这一点!

使用 getenforce 命令来显示 SELinux 的当前模式。如果你想要更改模式,使用 setenforce 0(设置为宽容模式)或 setenforce 1(强制模式)。

因为这些设置重启后就失效了,你需要编辑 /etc/selinux/config 配置文件并设置 SELINUX 变量为 enforcingpermissivedisabled ,保存设置让其重启后也有效:

如何启用和禁用 SELinux 模式

还有一点要注意,如果 getenforce 返回 Disabled,你得编辑 /etc/selinux/config 配置文件为你想要的操作模式并重启。否则你无法利用 setenforce 设置(或切换)操作模式。

setenforce 的典型用法之一包括在 SELinux 模式之间切换(从强制到宽容或相反)来定位一个应用是否行为不端或没有像预期一样工作。如果它在你将 SELinux 设置为宽容模式正常工作,你就可以确定你遇到的是 SELinux 权限问题。

有两种我们使用 SELinux 可能需要解决的典型案例:

  • 改变一个守护进程监听的默认端口。
  • 给一个虚拟主机设置 /var/www/html 以外的文档根路径值。

让我们用以下例子来看看这两种情况。

例 1:更改 sshd 守护进程的默认端口

大部分系统管理员为了加强服务器安全首先要做的事情之一就是更改 SSH 守护进程监听的端口,主要是为了阻止端口扫描和外部攻击。要达到这个目的,我们要更改 /etc/ssh/sshd_config 中的 Port 值为以下值(我们在这里使用端口 9999 为例):

Port 9999

在尝试重启服务并检查它的状态之后,我们会看到它启动失败:

# systemctl restart sshd
# systemctl status sshd

检查 SSH 服务状态

如果我们看看 /var/log/audit/audit.log,就会看到 sshd 被 SELinux 阻止在端口 9999 上启动,因为它是 JBoss 管理服务的保留端口(SELinux 日志信息包含了词语“AVC”,所以应该很容易把它同其他信息区分开来):

# cat /var/log/audit/audit.log | grep AVC | tail -1

检查 Linux 审计日志

在这种情况下大部分人可能会禁用 SELinux,但我们不这么做。我们会看到有个让 SELinux 和监听其他端口的 sshd 和谐共处的方法。首先确保你有 policycoreutils-python 这个包,执行:

# yum install policycoreutils-python

查看 SELinux 允许 sshd 监听的端口列表。在接下来的图片中我们还能看到端口 9999 是为其他服务保留的,所以我们暂时无法用它来运行其他服务:

# semanage port -l | grep ssh

当然我们可以给 SSH 选择其他端口,但如果我们确定我们不会使用这台机器跑任何 JBoss 相关的服务,我们就可以修改 SELinux 已存在的规则,转而给 SSH 分配那个端口:

# semanage port -m -t ssh_port_t -p tcp 9999

这之后,我们就可以用前一个 semanage 命令检查端口是否正确分配了,即使用 -lC 参数(list custom 的简称):

# semanage port -lC
# semanage port -l | grep ssh

给 SSH 分配端口

我们现在可以重启 SSH 服务并通过端口 9999 连接了。注意这个更改重启之后依然有效。

例 2:给一个虚拟主机设置 /var/www/html 以外的 文档根路径 DocumentRoot

如果你需要用除 /var/www/html 以外目录作为 文档根目录 DocumentRoot 设置一个 Apache 虚拟主机(也就是说,比如 /websrv/sites/gabriel/public_html):

DocumentRoot “/websrv/sites/gabriel/public_html”

Apache 会拒绝提供内容,因为 index.html 已经被标记为了 default_t SELinux 类型,Apache 无法访问它:

# wget http://localhost/index.html
# ls -lZ /websrv/sites/gabriel/public_html/index.html

被标记为 default\_t SELinux 类型

和之前的例子一样,你可以用以下命令验证这是不是 SELinux 相关的问题:

# cat /var/log/audit/audit.log | grep AVC | tail -1

检查日志确定是不是 SELinux 的问题

要将 /websrv/sites/gabriel/public_html 整个目录内容标记为 httpd_sys_content_t,执行:

# semanage fcontext -a -t httpd_sys_content_t "/websrv/sites/gabriel/public_html(/.*)?"

上面这个命令会赋予 Apache 对那个目录以及其内容的读取权限。

最后,要应用这条策略(并让更改的标记立即生效),执行:

# restorecon -R -v /websrv/sites/gabriel/public_html

现在你应该可以访问这个目录了:

# wget http://localhost/index.html

访问 Apache 目录

要获取关于 SELinux 的更多信息,参阅 Fedora 22 中的 SELinux 用户及管理员指南

AppArmor 介绍以及如何在 OpenSUSE 和 Ubuntu 上使用它

AppArmor 的操作是基于写在纯文本文件中的规则定义,该文件中含有允许权限和访问控制规则。安全配置文件用来限制应用程序如何与系统中的进程和文件进行交互。

系统初始就提供了一系列的配置文件,但其它的也可以由应用程序在安装的时候设置或由系统管理员手动设置。

像 SELinux 一样,AppArmor 以两种模式运行。在 强制 enforce 模式下,应用被赋予它们运行所需要的最小权限,但在 抱怨 complain 模式下 AppArmor 允许一个应用执行受限的操作并将操作造成的“抱怨”记录到日志里(/var/log/kern.log/var/log/audit/audit.log,和其它放在 /var/log/apparmor 中的日志)。

日志中会显示配置文件在强制模式下运行时会产生错误的记录,它们中带有 audit 这个词。因此,你可以在 AppArmor 的 强制 enforce 模式下运行之前,先在 抱怨 complain 模式下尝试运行一个应用并调整它的行为。

可以用这个命令显示 AppArmor 的当前状态:

$ sudo apparmor_status

查看 AppArmor 的状态

上面的图片指明配置 /sbin/dhclient/usr/sbin/,和 /usr/sbin/tcpdump 等处在 强制 enforce 模式下(在 Ubuntu 下默认就是这样的)。

因为不是所有的应用都包含相关的 AppArmor 配置,apparmor-profiles 包给其它没有提供限制的包提供了配置。默认它们配置在 抱怨 complain 模式下运行,以便系统管理员能够测试并选择一个所需要的配置。

我们将会利用 apparmor-profiles,因为写一份我们自己的配置已经超出了 LFCS 认证的范围了。但是,由于配置都是纯文本文件,你可以查看并学习它们,为以后创建自己的配置做准备。

AppArmor 配置保存在 /etc/apparmor.d 中。让我们来看看这个文件夹在安装 apparmor-profiles 之前和之后有什么不同:

$ ls /etc/apparmor.d

查看 AppArmor 文件夹内容

如果你再次执行 sudo apparmor_status,你会在 抱怨 complain 模式看到更长的配置文件列表。你现在可以执行下列操作。

将当前在 强制 enforce 模式下的配置文件切换到 抱怨 complain 模式:

$ sudo aa-complain /path/to/file

以及相反的操作(抱怨 –> 强制):

$ sudo aa-enforce /path/to/file

上面这些例子是允许使用通配符的。举个例子:

$ sudo aa-complain /etc/apparmor.d/*

会将 /etc/apparmor.d 中的所有配置文件设置为 抱怨 complain 模式,反之

$ sudo aa-enforce /etc/apparmor.d/*

会将所有配置文件设置为 强制 enforce 模式。

要完全禁用一个配置,在 /etc/apparmor.d/disabled 目录中创建一个符号链接:

$ sudo ln -s /etc/apparmor.d/profile.name /etc/apparmor.d/disable/

要获取关于 AppArmor 的更多信息,参阅官方的 AppArmor wiki 以及 Ubuntu 提供的文档。

总结

在这篇文章中我们学习了一些 SELinux 和 AppArmor 这两个著名的强制访问控制系统的基本知识。什么时候使用两者中的一个或是另一个?为了避免提高难度,你可能需要考虑专注于你选择的发行版自带的那一个。不管怎样,它们会帮助你限制进程和系统资源的访问,以提高你服务器的安全性。

关于本文你有任何的问题,评论,或建议,欢迎在下方发表。不要犹豫,让我们知道你是否有疑问或评论。


via: http://www.tecmint.com/mandatory-access-control-with-selinux-or-apparmor-linux/

作者:Gabriel Cánepa 译者:alim0x 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

本文讲述的是 Python 中 Mock 的使用。

如何执行单元测试而不用考验你的耐心

很多时候,我们编写的软件会直接与那些被标记为“垃圾”的服务交互。用外行人的话说:服务对我们的应用程序很重要,但是我们想要的是交互,而不是那些不想要的副作用,这里的“不想要”是在自动化测试运行的语境中说的。例如:我们正在写一个社交 app,并且想要测试一下 "发布到 Facebook" 的新功能,但是不想每次运行测试集的时候真的发布到 Facebook。

Python 的 unittest 库包含了一个名为 unittest.mock 或者可以称之为依赖的子包,简称为 mock —— 其提供了极其强大和有用的方法,通过它们可以 模拟 mock 并去除那些我们不希望的副作用。

注意:mock 最近被收录到了 Python 3.3 的标准库中;先前发布的版本必须通过 PyPI 下载 Mock 库。

恐惧系统调用

再举另一个例子,我们在接下来的部分都会用到它,这是就是系统调用。不难发现,这些系统调用都是主要的模拟对象:无论你是正在写一个可以弹出 CD 驱动器的脚本,还是一个用来删除 /tmp 下过期的缓存文件的 Web 服务,或者一个绑定到 TCP 端口的 socket 服务器,这些调用都是在你的单元测试上下文中不希望产生的副作用。

作为一个开发者,你需要更关心你的库是否成功地调用了一个可以弹出 CD 的系统函数(使用了正确的参数等等),而不是切身经历 CD 托盘每次在测试执行的时候都打开了。(或者更糟糕的是,弹出了很多次,在一个单元测试运行期间多个测试都引用了弹出代码!)

同样,保持单元测试的效率和性能意味着需要让如此多的“缓慢代码”远离自动测试,比如文件系统和网络访问。

对于第一个例子来说,我们要从原始形式换成使用 mock 重构一个标准 Python 测试用例。我们会演示如何使用 mock 写一个测试用例,使我们的测试更加智能、快速,并展示更多关于我们软件的工作原理。

一个简单的删除函数

我们都有过需要从文件系统中一遍又一遍的删除文件的时候,因此,让我们在 Python 中写一个可以使我们的脚本更加轻易完成此功能的函数。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os

def rm(filename):
    os.remove(filename)

很明显,我们的 rm 方法此时无法提供比 os.remove 方法更多的相关功能,但我们可以在这里添加更多的功能,使我们的基础代码逐步改善。

让我们写一个传统的测试用例,即,没有使用 mock

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from mymodule import rm

import os.path
import tempfile
import unittest

class RmTestCase(unittest.TestCase):

    tmpfilepath = os.path.join(tempfile.gettempdir(), "tmp-testfile")

    def setUp(self):
        with open(self.tmpfilepath, "wb") as f:
            f.write("Delete me!")

    def test_rm(self):
        # remove the file
        rm(self.tmpfilepath)
        # test that it was actually removed
        self.assertFalse(os.path.isfile(self.tmpfilepath), "Failed to remove the file.")

我们的测试用例相当简单,但是在它每次运行的时候,它都会创建一个临时文件并且随后删除。此外,我们没有办法测试我们的 rm 方法是否正确地将我们的参数向下传递给 os.remove 调用。我们可以基于以上的测试认为它做到了,但还有很多需要改进的地方。

使用 Mock 重构

让我们使用 mock 重构我们的测试用例:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from mymodule import rm

import mock
import unittest

class RmTestCase(unittest.TestCase):

    @mock.patch('mymodule.os')
    def test_rm(self, mock_os):
        rm("any path")
        # test that rm called os.remove with the right parameters
        mock_os.remove.assert_called_with("any path")

使用这些重构,我们从根本上改变了测试用例的操作方式。现在,我们有一个可以用于验证其他功能的内部对象。

潜在陷阱

第一件需要注意的事情就是,我们使用了 mock.patch 方法装饰器,用于模拟位于 mymodule.os 的对象,并且将 mock 注入到我们的测试用例方法。那么只是模拟 os 本身,而不是 mymodule.osos 的引用(LCTT 译注:注意 @mock.patch('mymodule.os') 便是模拟 mymodule.os 下的 os),会不会更有意义呢?

当然,当涉及到导入和管理模块,Python 的用法就像蛇一样灵活。在运行时,mymodule 模块有它自己的被导入到本模块局部作用域的 os。因此,如果我们模拟 os,我们是看不到 mock 在 mymodule 模块中的模仿作用的。

这句话需要深刻地记住:

模拟一个东西要看它用在何处,而不是来自哪里。

如果你需要为 myproject.app.MyElaborateClass 模拟 tempfile 模块,你可能需要将 mock 用于 myproject.app.tempfile,而其他模块保持自己的导入。

先将那个陷阱放一边,让我们继续模拟。

向 ‘rm’ 中加入验证

之前定义的 rm 方法相当的简单。在盲目地删除之前,我们倾向于验证一个路径是否存在,并验证其是否是一个文件。让我们重构 rm 使其变得更加智能:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import os.path

def rm(filename):
    if os.path.isfile(filename):
        os.remove(filename)

很好。现在,让我们调整测试用例来保持测试的覆盖率。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from mymodule import rm

import mock
import unittest

class RmTestCase(unittest.TestCase):

    @mock.patch('mymodule.os.path')
    @mock.patch('mymodule.os')
    def test_rm(self, mock_os, mock_path):
        # set up the mock
        mock_path.isfile.return_value = False

        rm("any path")

        # test that the remove call was NOT called.
        self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.")

        # make the file 'exist'
        mock_path.isfile.return_value = True

        rm("any path")

        mock_os.remove.assert_called_with("any path")

我们的测试用例完全改变了。现在我们可以在没有任何副作用的情况下核实并验证方法的内部功能。

将文件删除作为服务

到目前为止,我们只是将 mock 应用在函数上,并没应用在需要传递参数的对象和实例的方法上。我们现在开始涵盖对象的方法。

首先,我们将 rm 方法重构成一个服务类。实际上将这样一个简单的函数转换成一个对象,在本质上这不是一个合理的需求,但它能够帮助我们了解 mock 的关键概念。让我们开始重构:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import os.path

class RemovalService(object):
    """A service for removing objects from the filesystem."""

    def rm(filename):
        if os.path.isfile(filename):
            os.remove(filename)

你会注意到我们的测试用例没有太大变化:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from mymodule import RemovalService

import mock
import unittest

class RemovalServiceTestCase(unittest.TestCase):

    @mock.patch('mymodule.os.path')
    @mock.patch('mymodule.os')
    def test_rm(self, mock_os, mock_path):
        # instantiate our service
        reference = RemovalService()

        # set up the mock
        mock_path.isfile.return_value = False

        reference.rm("any path")

        # test that the remove call was NOT called.
        self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.")

        # make the file 'exist'
        mock_path.isfile.return_value = True

        reference.rm("any path")

        mock_os.remove.assert_called_with("any path")

很好,我们知道 RemovalService 会如预期般的工作。接下来让我们创建另一个服务,将 RemovalService 声明为它的一个依赖:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import os.path

class RemovalService(object):
    """A service for removing objects from the filesystem."""

    def rm(self, filename):
        if os.path.isfile(filename):
            os.remove(filename)


class UploadService(object):

    def __init__(self, removal_service):
        self.removal_service = removal_service

    def upload_complete(self, filename):
        self.removal_service.rm(filename)

因为我们的测试覆盖了 RemovalService,因此我们不会对我们测试用例中 UploadService 的内部函数 rm 进行验证。相反,我们将调用 UploadServiceRemovalService.rm 方法来进行简单测试(当然没有其他副作用),我们通过之前的测试用例便能知道它可以正确地工作。

这里有两种方法来实现测试:

  1. 模拟 RemovalService.rm 方法本身。
  2. 在 UploadService 的构造函数中提供一个模拟实例。

因为这两种方法都是单元测试中非常重要的方法,所以我们将同时对这两种方法进行回顾。

方法 1:模拟实例的方法

mock 库有一个特殊的方法装饰器,可以模拟对象实例的方法和属性,即 @mock.patch.object decorator 装饰器:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from mymodule import RemovalService, UploadService

import mock
import unittest

class RemovalServiceTestCase(unittest.TestCase):

    @mock.patch('mymodule.os.path')
    @mock.patch('mymodule.os')
    def test_rm(self, mock_os, mock_path):
        # instantiate our service
        reference = RemovalService()

        # set up the mock
        mock_path.isfile.return_value = False

        reference.rm("any path")

        # test that the remove call was NOT called.
        self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.")

        # make the file 'exist'
        mock_path.isfile.return_value = True

        reference.rm("any path")

        mock_os.remove.assert_called_with("any path")


class UploadServiceTestCase(unittest.TestCase):

    @mock.patch.object(RemovalService, 'rm')
    def test_upload_complete(self, mock_rm):
        # build our dependencies
        removal_service = RemovalService()
        reference = UploadService(removal_service)

        # call upload_complete, which should, in turn, call `rm`:
        reference.upload_complete("my uploaded file")

        # check that it called the rm method of any RemovalService
        mock_rm.assert_called_with("my uploaded file")

        # check that it called the rm method of _our_ removal_service
        removal_service.rm.assert_called_with("my uploaded file")

非常棒!我们验证了 UploadService 成功调用了我们实例的 rm 方法。你是否注意到一些有趣的地方?这种修补机制(patching mechanism)实际上替换了我们测试用例中的所有 RemovalService 实例的 rm 方法。这意味着我们可以检查实例本身。如果你想要了解更多,可以试着在你模拟的代码下断点,以对这种修补机制的原理获得更好的认识。

陷阱:装饰顺序

当我们在测试方法中使用多个装饰器,其顺序是很重要的,并且很容易混乱。基本上,当装饰器被映射到方法参数时,装饰器的工作顺序是反向的。思考这个例子:

    @mock.patch('mymodule.sys')
    @mock.patch('mymodule.os')
    @mock.patch('mymodule.os.path')
    def test_something(self, mock_os_path, mock_os, mock_sys):
        pass

注意到我们的参数和装饰器的顺序是反向匹配了吗?这部分是由 Python 的工作方式所导致的。这里是使用多个装饰器的情况下它们执行顺序的伪代码:

patch_sys(patch_os(patch_os_path(test_something)))

因为 sys 补丁位于最外层,所以它最晚执行,使得它成为实际测试方法参数的最后一个参数。请特别注意这一点,并且在运行你的测试用例时,使用调试器来保证正确的参数以正确的顺序注入。

方法 2:创建 Mock 实例

我们可以使用构造函数为 UploadService 提供一个 Mock 实例,而不是模拟特定的实例方法。我更推荐方法 1,因为它更加精确,但在多数情况,方法 2 或许更加有效和必要。让我们再次重构测试用例:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from mymodule import RemovalService, UploadService

import mock
import unittest

class RemovalServiceTestCase(unittest.TestCase):

    @mock.patch('mymodule.os.path')
    @mock.patch('mymodule.os')
    def test_rm(self, mock_os, mock_path):
        # instantiate our service
        reference = RemovalService()

        # set up the mock
        mock_path.isfile.return_value = False

        reference.rm("any path")

        # test that the remove call was NOT called.
        self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.")

        # make the file 'exist'
        mock_path.isfile.return_value = True

        reference.rm("any path")

        mock_os.remove.assert_called_with("any path")


class UploadServiceTestCase(unittest.TestCase):

    def test_upload_complete(self, mock_rm):
        # build our dependencies
        mock_removal_service = mock.create_autospec(RemovalService)
        reference = UploadService(mock_removal_service)

        # call upload_complete, which should, in turn, call `rm`:
        reference.upload_complete("my uploaded file")

        # test that it called the rm method
        mock_removal_service.rm.assert_called_with("my uploaded file")

在这个例子中,我们甚至不需要修补任何功能,只需为 RemovalService 类创建一个 auto-spec,然后将实例注入到我们的 UploadService 以验证功能。

mock.create_autospec 方法为类提供了一个同等功能实例。实际上来说,这意味着在使用返回的实例进行交互的时候,如果使用了非法的方式将会引发异常。更具体地说,如果一个方法被调用时的参数数目不正确,将引发一个异常。这对于重构来说是非常重要。当一个库发生变化的时候,中断测试正是所期望的。如果不使用 auto-spec,尽管底层的实现已经被破坏,我们的测试仍然会通过。

陷阱:mock.Mock 和 mock.MagicMock 类

mock 库包含了两个重要的类 mock.Mockmock.MagicMock,大多数内部函数都是建立在这两个类之上的。当在选择使用 mock.Mock 实例、mock.MagicMock 实例还是 auto-spec 的时候,通常倾向于选择使用 auto-spec,因为对于未来的变化,它更能保持测试的健全。这是因为 mock.Mockmock.MagicMock 会无视底层的 API,接受所有的方法调用和属性赋值。比如下面这个用例:

class Target(object):
    def apply(value):
        return value

def method(target, value):
    return target.apply(value)

我们可以像下面这样使用 mock.Mock 实例进行测试:

class MethodTestCase(unittest.TestCase):

    def test_method(self):
        target = mock.Mock()

        method(target, "value")

        target.apply.assert_called_with("value")

这个逻辑看似合理,但如果我们修改 Target.apply 方法接受更多参数:

class Target(object):
    def apply(value, are_you_sure):
        if are_you_sure:
            return value
        else:
            return None

重新运行你的测试,你会发现它仍能通过。这是因为它不是针对你的 API 创建的。这就是为什么你总是应该使用 create_autospec 方法,并且在使用 @patch@patch.object 装饰方法时使用 autospec 参数。

现实例子:模拟 Facebook API 调用

作为这篇文章的结束,我们写一个更加适用的现实例子,一个在介绍中提及的功能:发布消息到 Facebook。我将写一个不错的包装类及其对应的测试用例。

import facebook

class SimpleFacebook(object):

    def __init__(self, oauth_token):
        self.graph = facebook.GraphAPI(oauth_token)

    def post_message(self, message):
        """Posts a message to the Facebook wall."""
        self.graph.put_object("me", "feed", message=message)

这是我们的测试用例,它可以检查我们发布的消息,而不是真正地发布消息:

import facebook
import simple_facebook
import mock
import unittest

class SimpleFacebookTestCase(unittest.TestCase):

    @mock.patch.object(facebook.GraphAPI, 'put_object', autospec=True)
    def test_post_message(self, mock_put_object):
        sf = simple_facebook.SimpleFacebook("fake oauth token")
        sf.post_message("Hello World!")

        # verify
        mock_put_object.assert_called_with(message="Hello World!")

正如我们所看到的,在 Python 中,通过 mock,我们可以非常容易地动手写一个更加智能的测试用例。

Python Mock 总结

即使对它的使用还有点不太熟悉,对单元测试来说,Python 的 mock 库可以说是一个规则改变者。我们已经演示了常见的用例来了解了 mock 在单元测试中的使用,希望这篇文章能够帮助 Python 开发者克服初期的障碍,写出优秀、经受过考验的代码。


via: https://www.toptal.com/python/an-introduction-to-mocking-in-python

作者:NAFTULI TZVI KAY 译者:cposture 校对:wxy

本文由 LCTT 原创翻译,Linux中国 荣誉推出

OpenStack 欢迎新成员的到来,但是,对于这个发展趋近成熟并且快速迭代的开源社区而言,能够拥有一个新手指南并不是件坏事。在奥斯汀举办的 OpenStack 峰会上,Paul Belanger (来自红帽公司)、 Elizabeth K. Joseph (来自 HPE 公司)和 Christopher Aedo (来自 IBM 公司)就针对新人的 OpenStack 架构作了一场专门的讲演。在这次采访中,他们提供了一些建议和资源来帮助新人成为 OpenStack 贡献者中的一员。

你的讲演介绍中说你将“深入架构核心,并解释你需要知道的关于让 OpenStack 工作起来的每一件事情”。这对于 40 分钟的讲演来说是一个艰巨的任务。那么,对于学习 OpenStack 架构的新手来说最需要知道那些事情呢?

Elizabeth K. Joseph (EKJ): 我们没有为 OpenStack 使用 GitHub 这种提交补丁的方式,这是因为这样做会对新手造成巨大的困扰,尽管由于历史原因我们还是在 GitHub 上维护了所有库的一个镜像。相反,我们使用了一种完全开源的评审形式,而且持续集成(CI)是由 OpenStack 架构团队维护的。与之有关的,自从我们使用了 CI 系统,每一个提交给 OpenStack 的改变都会在被合并之前进行测试。

Paul Belanger (PB): 这个项目中的大多数都是富有激情的人,因此当你提交的补丁被某个人否定时不要感到沮丧。

Christopher Aedo (CA):社区会帮助你取得成功,因此不要害怕提问或者寻求更多的那些能够促进你理解某些事物的引导者。

在你的讲话中,对于一些你无法涉及到的方面,你会向新手推荐哪些在线资源来让他们更加容易入门?

PB:当然是我们的 OpenStack 项目架构文档。我们已经花了足够大的努力来尽可能让这些文档能够随时保持最新状态。在 OpenStack 运行中使用的每个系统都作为一个项目,都制作了专门的页面来进行说明。甚至于连 OpenStack 云这种架构团队也会放到线上。

EKJ:我对于架构文档这件事上的观点和 Paul 是一致的,另外,我们十分乐意看到来自那些正在学习项目的人们提交上来的补丁。我们通常不会意识到我们忽略了文档中的某些内容,除非它们恰好被人问起。因此,阅读、学习,会帮助我们修补这些知识上的漏洞。你可以在 [OpenStack 架构邮件清单]提出你的问题,或者在我们位于 FreeNode 上的 #OpenStack-infra 的 IRC 专栏发起你的提问。

CA:我喜欢这个详细的帖子,它是由 Ian Wienand 写的一篇关于构建镜像的文章。

"gotchas" 会是 OpenStack 新的贡献者们所寻找的吗?

EKJ:向项目作出贡献并不仅仅是提交新的代码和新的特性;OpenStack 社区高度重视代码评审。如果你想要别人查看你的补丁,那你最好先看看其他人是如何做的,然后参考他们的风格,最后一步步做到你也能够向其他人一样提交清晰且结构分明的代码补丁。你越是能让你的同伴了解你的工作并知道你正在做的评审,那他们也就越有可能及时评审你的代码。

CA:我看到过大量的新手在面对 Gerrit 时受挫,阅读开发者引导中的开发者工作步骤,有可能的话多读几遍。如果你没有用过 Gerrit,那你最初对它的感觉可能是困惑和无力的。但是,如果你随后做了一些代码评审的工作,那么你就能轻松应对它。此外,我是 IRC 的忠实粉丝,它可能是一个获得帮助的好地方,但是,你最好保持一个长期在线的状态,这样,尽管你在某个时候没有出现,人们也可以回答你的问题。(阅读 IRC,开源成功的秘诀)你不必总是在线,但是你最好能够轻松的在一个频道中回溯之前信息,以此来跟上最新的动态,这种能力非常重要。

PB:我同意 Elizabeth 和 Chris 的观点, Gerrit 是需要花点精力的,它将汇聚你的开发方面的努力。你不仅仅要提交代码给别人去评审,同时,你也要能够评审其他人的代码。看到 Gerrit 的界面,你可能一时会变的很困惑。我推荐新手去尝试 Gertty,它是一个基于控制台的终端界面,用于 Gerrit 代码评审系统,而它恰好也是 OpenStack 架构所驱动的一个项目。

你对于 OpenStack 新手如何通过网络与其他贡献者交流方面有什么好的建议?

PB:对我来说,是通过 IRC 并在 Freenode 上参加 #OpenStack-infra 频道(IRC 日志)。这频道上面有很多对新手来说很有价值的资源。你可以看到 OpenStack 项目日复一日的运作情况,同时,一旦你知道了 OpenStack 项目的工作原理,你将更好的知道如何为 OpenStack 的未来发展作出贡献。

CA:我想要为 IRC 再次说明一点,在 IRC 上保持全天在线记录对我来说有非常重大的意义,因为我会时刻保持连接并随时接到提醒。这也是一种非常好的获得帮助的方式,特别是当你和某人卡在了项目中出现的某一个难题的时候,而在一个活跃的 IRC 频道中,总会有一些人很乐意为你解决问题。

EKJOpenStack 开发邮件列表对于能够时刻查看到你所致力于的 OpenStack 项目的最新情况是非常重要的。因此,我推荐一定要订阅它。邮件列表使用主题标签来区分项目,因此你可以设置你的邮件客户端来使用它来专注于你所关心的项目。除了在线资源之外,全世界范围内也成立了一些 OpenStack 小组,他们被用来为 OpenStack 的用户和贡献者提供服务。这些小组可能会定期要求 OpenStack 主要贡献者们举办座谈和活动。你可以在 MeetUp.com 上搜素你所在地域的贡献者活动聚会,或者在 groups.openstack.org 上查看你所在的地域是否存在 OpenStack 小组。最后,还有一个每六个月举办一次的 OpenStack 峰会,这个峰会上会作一些关于架构的演说。当前状态下,这个峰会包含了用户会议和开发者会议,会议内容都是和 OpenStack 相关的东西,包括它的过去,现在和未来。

OpenStack 需要在那些方面得到提升来让新手更加容易学会并掌握?

PB: 我认为我们的 account-setup 环节对于新的贡献者已经做的比较容易了,特别是教他们如何提交他们的第一个补丁。真正参与到 OpenStack 开发者模式中是需要花费很大的努力的,相比贡献者来说已经显得非常多了;然而,一旦融入进去了,这个模式将会运转的十分高效和令人满意。

CA: 我们拥有一个由专业开发者组成的社区,而且我们的关注点都是发展 OpenStack 本身,同时,我们致力于让用户付出更小的代价去使用 OpenStack 云架构平台。我们需要发掘更多的应用开发者,并且鼓励更多的人去开发能在 OpenStack 云上完美运行的云应用程序,我们还鼓励他们在社区 App 目录上去贡献那些由他们开发的应用。我们可以通过不断提升我们的 API 标准和保证我们不同的库(比如 libcloud,phpopencloud 以及其他一些库)持续地为开发者提供可信赖的支持来实现这一目标。还有一点就是通过举办更多的 OpenStack 黑客比赛。所有的这些事情都可以降低新人的学习门槛,这样也能引导他们与这个社区之间的关系更加紧密。

EKJ: 我已经致力于开源软件很多年了。但是,对于大量的 OpenStack 开发者而言,这是一个他们自己所从事的第一个开源项目。我发现他们之前使用私有软件的背景并没有为他们塑造开源的观念、方法论,以及在开源项目中需要具备的合作技巧。我乐于看到我们能够让那些曾经一直在使用私有软件工作的人能够真正的明白他们在开源如软件社区所从事的事情的巨大价值。

我想把 2016 年打造成开源俳句之年。请用俳句来向新手解释 OpenStack 一下。

(LCTT 译注:俳句(Haiku)是一种日本古典短诗,以5-7-5音节为三句,校对者不揣浅陋,诌了几句歪诗,勿笑 :D,另外 OpenStack 本身音节太长,就捏造了一个中文译名“开栈”——明白就好。)

PB 开栈在云上 OpenStack runs clouds // 倘钟情自由软件 If you enjoy free software // 先当造补丁 Submit your first patch

CA 时光不必久 In the near future // 开栈将支配世界 OpenStack will rule the world // 协力早来到 Help make it happen!

EKJ 开栈有自由 OpenStack is free // 放在自家服务器 Deploy on your own servers // 运行你的云 And run your own cloud!

Paul、Elizabeth 和 Christopher 在 4 月 25 号星期一上午 11:15 于奥斯汀举办的 OpenStack 峰会发表了演说


via: https://opensource.com/business/16/4/interview-openstack-infrastructure-beginners

作者:Rikki Endsley 译者:kylepeng93 校对:wxy

本文由 LCTT 原创翻译,Linux中国 荣誉推出