trpl-zh-cn/docs/ch12-04-testing-the-librarys-functionality.html

344 lines
27 KiB
HTML
Raw Normal View History

2017-03-02 23:36:03 +08:00
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>测试库的功能 - Rust 程序设计语言 简体中文版</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="Rust 程序设计语言 简体中文版">
<meta name="viewport" content="width=device-width, initial-scale=1">
<base href="">
<link rel="stylesheet" href="book.css">
<link href='https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800' rel='stylesheet' type='text/css'>
<link rel="shortcut icon" href="favicon.png">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
<link rel="stylesheet" href="highlight.css">
<link rel="stylesheet" href="tomorrow-night.css">
<!-- MathJax -->
<script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<!-- Fetch JQuery from CDN but have a local fallback -->
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script>
if (typeof jQuery == 'undefined') {
document.write(unescape("%3Cscript src='jquery.js'%3E%3C/script%3E"));
}
</script>
</head>
<body class="light">
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme = localStorage.getItem('theme');
if (theme == null) { theme = 'light'; }
$('body').removeClass().addClass(theme);
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
var sidebar = localStorage.getItem('sidebar');
if (sidebar === "hidden") { $("html").addClass("sidebar-hidden") }
else if (sidebar === "visible") { $("html").addClass("sidebar-visible") }
</script>
<div id="sidebar" class="sidebar">
2017-05-31 23:48:57 +08:00
<ul class="chapter"><li><a href="ch01-00-introduction.html"><strong>1.</strong> 介绍</a></li><li><ul class="section"><li><a href="ch01-01-installation.html"><strong>1.1.</strong> 安装</a></li><li><a href="ch01-02-hello-world.html"><strong>1.2.</strong> Hello, World!</a></li></ul></li><li><a href="ch02-00-guessing-game-tutorial.html"><strong>2.</strong> 猜猜看教程</a></li><li><a href="ch03-00-common-programming-concepts.html"><strong>3.</strong> 通用编程概念</a></li><li><ul class="section"><li><a href="ch03-01-variables-and-mutability.html"><strong>3.1.</strong> 变量和可变性</a></li><li><a href="ch03-02-data-types.html"><strong>3.2.</strong> 数据类型</a></li><li><a href="ch03-03-how-functions-work.html"><strong>3.3.</strong> 函数如何工作</a></li><li><a href="ch03-04-comments.html"><strong>3.4.</strong> 注释</a></li><li><a href="ch03-05-control-flow.html"><strong>3.5.</strong> 控制流</a></li></ul></li><li><a href="ch04-00-understanding-ownership.html"><strong>4.</strong> 认识所有权</a></li><li><ul class="section"><li><a href="ch04-01-what-is-ownership.html"><strong>4.1.</strong> 什么是所有权</a></li><li><a href="ch04-02-references-and-borrowing.html"><strong>4.2.</strong> 引用 &amp; 借用</a></li><li><a href="ch04-03-slices.html"><strong>4.3.</strong> Slices</a></li></ul></li><li><a href="ch05-00-structs.html"><strong>5.</strong> 结构体</a></li><li><ul class="section"><li><a href="ch05-01-method-syntax.html"><strong>5.1.</strong> 方法语法</a></li></ul></li><li><a href="ch06-00-enums.html"><strong>6.</strong> 枚举和模式匹配</a></li><li><ul class="section"><li><a href="ch06-01-defining-an-enum.html"><strong>6.1.</strong> 定义枚举</a></li><li><a href="ch06-02-match.html"><strong>6.2.</strong> <code>match</code>控制流运算符</a></li><li><a href="ch06-03-if-let.html"><strong>6.3.</strong> <code>if let</code>简单控制流</a></li></ul></li><li><a href="ch07-00-modules.html"><strong>7.</strong> 模块</a></li><li><ul class="section"><li><a href="ch07-01-mod-and-the-filesystem.html"><strong>7.1.</strong> <code>mod</code>和文件系统</a></li><li><a href="ch07-02-controlling-visibility-with-pub.html"><strong>7.2.</strong> 使用<code>pub</code>控制可见性</a></li><li><a href="ch07-03-importing-names-with-use.html"><strong>7.3.</strong> 使用<code>use</code>导入命名</a></li></ul></li><li><a href="ch08-00-common-collections.html"><strong>8.</strong> 通用集合类型</a></li><li><ul class="section"><li><a href="ch08-01-vectors.html"><strong>8.1.</strong> vector</a></li><li><a href="ch08-02-strings.html"><strong>8.2.</strong> 字符串</a></li><li><a href="ch08-03-hash-maps.html"><strong>8.3.</strong> 哈希 map</a></li></ul></li><li><a href="ch09-00-error-handling.html"><strong>9.</strong> 错误处理</a></li><li><ul class="section"><li><a href="ch09-01-unrecoverable-errors-with-panic.html"><strong>9.1.</strong> <code>panic!</code>与不可恢复的错误</a></li><li><a href="ch09-02-recoverable-errors-with-result.html"><strong>9.2.</strong> <code>Result</code>与可恢复的错误</a></li><li><a href="ch09-03-to-panic-or-not-to-panic.html"><strong>9.3.</strong> <code>panic!</code>还是不<code>panic!</code></a></li></ul></li><li><a href="ch10-00-generics.html"><strong>10.</strong> 泛型、trait 和生命周期</a></li><li><ul class="section"><li><a href="ch10-01-syntax.html"><strong>10.1.</strong> 泛型数据类型</a></li><li><a href="ch10-02-traits.html"><strong>10.2.</strong> trait定义共享的行为</a></li><li><a href="ch10-03-lifetime-syntax.html"><strong>10.3.</strong> 生命周期与引用有效性</a></li></ul></li><li><a href="ch11-00-testing.html"><strong>11.</strong> 测试</a></li><li><ul class="section"><li><a href="ch11-01-writing-tests.html"><strong>11.1.</strong> 编写测试</a></li><li><a href="ch11-02-running-tests.html"><strong>11.2.</strong> 运行测试</a></li><li><a href="ch11-03-test-organization.html"><strong>11.3.</strong> 测试的组织结构</a></li></ul></li><li><a href="ch12-00-an-io-project.h
2017-03-02 23:36:03 +08:00
</div>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar" class="menu-bar">
<div class="left-buttons">
<i id="sidebar-toggle" class="fa fa-bars"></i>
<i id="theme-toggle" class="fa fa-paint-brush"></i>
</div>
<h1 class="menu-title">Rust 程序设计语言 简体中文版</h1>
<div class="right-buttons">
<i id="print-button" class="fa fa-print" title="Print this book"></i>
</div>
</div>
<div id="content" class="content">
2017-03-06 22:56:55 +08:00
<a class="header" href="#测试库的功能" name="测试库的功能"><h2>测试库的功能</h2></a>
<blockquote>
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-04-testing-the-librarys-functionality.md">ch12-04-testing-the-librarys-functionality.md</a>
<br>
2017-04-18 14:55:09 +08:00
commit b8e4fcbf289b82c12121b282747ce05180afb1fb</p>
2017-03-06 22:56:55 +08:00
</blockquote>
2017-04-18 14:55:09 +08:00
<p>现在我们将逻辑提取到了 <em>src/lib.rs</em> 并将所有的参数解析和错误处理留在了 <em>src/main.rs</em> 中,为代码的核心功能编写测试将更加容易。我们可以直接使用多种参数调用函数并检查返回值而无需从命令行运行二进制文件了。</p>
<p>在这一部分我们将遵循测试驱动开发Test Driven Development, TTD的模式。这是一个软件开发技术它遵循如下步骤</p>
<ol>
<li>编写一个会失败的测试,并运行它以确保其因为你期望的原因失败。</li>
<li>编写或修改刚好足够的代码来使得新的测试通过。</li>
<li>重构刚刚增加或修改的代码,并确保测试仍然能通过。</li>
<li>重复上述步骤!</li>
</ol>
<p>这只是众多编写软件的方法之一,不过 TDD 有助于驱动代码的设计。在编写能使测试通过的代码之前编写测测试有助于在开发过程中保持高测试覆盖率。</p>
<p>我们将测试驱动实现<code>greprs</code>实际在文件内容中搜索查询字符串并返回匹配的行列表的部分。我们将在一个叫做<code>search</code>的函数中增加这些功能。</p>
<a class="header" href="#编写失败测试" name="编写失败测试"><h3>编写失败测试</h3></a>
<p>首先,去掉 <em>src/lib.rs</em><em>src/main.rs</em> 中的<code>println!</code>语句,因为不再真的需要他们了。接着我们会像第十一章那样增加一个<code>test</code>模块和一个测试函数。测试函数指定了我们希望<code>search</code>函数拥有的行为:它会获取一个需要查询的字符串和用来查询的文本。列表 12-15 展示了这个测试:</p>
2017-03-27 16:17:20 +08:00
<p><span class="filename">Filename: src/lib.rs</span></p>
2017-04-18 14:55:09 +08:00
<pre><code class="language-rust"># fn search&lt;'a&gt;(query: &amp;str, contents: &amp;'a str) -&gt; Vec&lt;&amp;'a str&gt; {
# vec![]
2017-03-06 22:56:55 +08:00
# }
#
#[cfg(test)]
mod test {
2017-04-18 14:55:09 +08:00
use super::*;
2017-03-06 22:56:55 +08:00
#[test]
fn one_result() {
2017-04-18 14:55:09 +08:00
let query = &quot;duct&quot;;
2017-03-06 22:56:55 +08:00
let contents = &quot;\
Rust:
safe, fast, productive.
Pick three.&quot;;
assert_eq!(
vec![&quot;safe, fast, productive.&quot;],
2017-04-18 14:55:09 +08:00
search(query, contents)
2017-03-06 22:56:55 +08:00
);
}
}
</code></pre>
2017-04-18 14:55:09 +08:00
<p><span class="caption">Listing 12-15: Creating a failing test for the <code>search</code>
function we wish we had</span></p>
<p>这里选择使用 &quot;duct&quot; 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 &quot;duct&quot;。我们断言<code>search</code>函数的返回值只包含期望的那一行。</p>
<p>我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译!我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的<code>search</code>函数定义,如列表 12-16 所示。一旦有了它,这个测试应该能够编译并因为空 vector 并不匹配一个包含一行<code>&quot;safe, fast, productive.&quot;</code>的 vector 而失败。</p>
<p><span class="filename">Filename: src/lib.rs</span></p>
<pre><code class="language-rust">fn search&lt;'a&gt;(query: &amp;str, contents: &amp;'a str) -&gt; Vec&lt;&amp;'a str&gt; {
vec![]
}
</code></pre>
<p><span class="caption">Listing 12-16: Defining just enough of the <code>search</code>
function that our test will compile</span></p>
2017-03-06 22:56:55 +08:00
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
2017-04-18 14:55:09 +08:00
<p>注意需要在<code>search</code>的签名中显式定义一个显式生命周期<code>'a</code>并用于<code>contents</code>参数和返回值。回忆一下第十章中生命周期参数指定哪个参数的生命周期与返回值的生命周期相关联。在这个例子中,我们表明返回的 vector 中应该包含引用参数<code>contents</code>(而不是参数<code>query</code> slice 的字符串 slice。</p>
<p>换句话说,我们告诉 Rust 函数<code>search</code>返回的数据将与<code>search</code>函数中的参数<code>contents</code>的数据存在的一样久。这是非常重要的!为了使这个引用有效那么<strong></strong>slice 引用的数据也需要保持有效;如果编译器认为我们是在创建<code>query</code>而不是<code>contents</code>的字符串 slice那么安全检查将是不正确的。</p>
<p>如果尝试不用生命周期编译的话,我们将得到如下错误:</p>
2017-03-06 22:56:55 +08:00
<pre><code>error[E0106]: missing lifetime specifier
2017-04-18 14:55:09 +08:00
--&gt; src/lib.rs:5:47
|
5 | fn search(query: &amp;str, contents: &amp;str) -&gt; Vec&lt;&amp;str&gt; {
| ^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `query` or `contents`
2017-03-06 22:56:55 +08:00
</code></pre>
<p>Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数<code>contents</code>包含了所有的文本而且我们希望返回匹配的那部分文本,而我们知道<code>contents</code>是应该要使用生命周期语法来与返回值相关联的参数。</p>
2017-04-18 14:55:09 +08:00
<p>其他语言中并不需要你在函数签名中将参数与返回值相关联,所以这么做可能仍然感觉有些陌生,随着时间的推移会越来越容易。你可能想要将这个例子与第十章中生命周期语法部分做对比。</p>
<p>现在试尝试运行测试:</p>
2017-03-06 22:56:55 +08:00
<pre><code>$ cargo test
...warnings...
Finished debug [unoptimized + debuginfo] target(s) in 0.43 secs
Running target/debug/deps/greprs-abcabcabc
running 1 test
test test::one_result ... FAILED
failures:
---- test::one_result stdout ----
thread 'test::one_result' panicked at 'assertion failed: `(left == right)`
(left: `[&quot;safe, fast, productive.&quot;]`, right: `[]`)', src/lib.rs:16
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
test::one_result
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
error: test failed
</code></pre>
2017-04-18 14:55:09 +08:00
<p>好的,测试失败了,这正是我们所期望的。修改代码来让测试通过吧!</p>
<a class="header" href="#编写使测试通过的代码" name="编写使测试通过的代码"><h3>编写使测试通过的代码</h3></a>
<p>目前测试之所以会失败是因为我们总是返回一个空的 vector。为了修复并实现<code>search</code>,我们的程序需要遵循如下步骤:</p>
2017-03-06 22:56:55 +08:00
<ol>
<li>遍历每一行文本。</li>
<li>查看这一行是否包含要搜索的字符串。
<ul>
2017-04-18 14:55:09 +08:00
<li>如果有,将这一行加入返回列表中。</li>
<li>如果没有,什么也不做。</li>
2017-03-06 22:56:55 +08:00
</ul>
</li>
<li>返回匹配到的列表</li>
</ol>
2017-04-18 14:55:09 +08:00
<p>让我们一步一步的来,从遍历每行开始。</p>
<a class="header" href="#使用lines方法遍历每一行" name="使用lines方法遍历每一行"><h4>使用<code>lines</code>方法遍历每一行</h4></a>
2017-04-24 23:03:13 +08:00
<p>Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被命名为<code>lines</code>,它如列表 12-17 这样工作:</p>
2017-03-06 22:56:55 +08:00
<p><span class="filename">Filename: src/lib.rs</span></p>
2017-04-18 14:55:09 +08:00
<pre><code class="language-rust,ignore">fn search&lt;'a&gt;(query: &amp;str, contents: &amp;'a str) -&gt; Vec&lt;&amp;'a str&gt; {
2017-03-06 22:56:55 +08:00
for line in contents.lines() {
// do something with line
}
}
</code></pre>
2017-04-18 14:55:09 +08:00
<p><span class="caption">Listing 12-17: Iterating through each line in
<code>contents</code></span></p>
2017-03-06 22:56:55 +08:00
<!-- Will add wingdings in libreoffice /Carol -->
2017-05-15 08:27:11 +08:00
<p><code>lines</code>方法返回一个迭代器。第十三章会深入了解迭代器,不过我们已经在列表 3-6 中见过使用迭代器的方法,在那里使用了一个<code>for</code>循环和迭代器在一个集合的每一项上运行一些代码。</p>
2017-04-18 14:55:09 +08:00
<!-- so what does `lines` do on its own, if we need to use it in a for loop to
work? -->
<!-- It does nothing on its own, it returns an iterator for you to do something
with. Here, the thing we're doing with it is using it with a `for` loop. I'm
not sure exactly what you're asking or how to make the text clearer, but I
added a reference to where we've done this in the book previously. /Carol -->
<a class="header" href="#用查询字符串搜索每一行" name="用查询字符串搜索每一行"><h4>用查询字符串搜索每一行</h4></a>
<p>接下来将会增加检查当前行是否包含查询字符串的功能。幸运的是,字符串类型为此也有一个有用的方法叫做<code>contains</code>!如列表 12-18 所示在<code>search</code>函数中加入<code>contains</code>方法:</p>
2017-03-06 22:56:55 +08:00
<p><span class="filename">Filename: src/lib.rs</span></p>
2017-04-18 14:55:09 +08:00
<pre><code class="language-rust,ignore">fn search&lt;'a&gt;(query: &amp;str, contents: &amp;'a str) -&gt; Vec&lt;&amp;'a str&gt; {
2017-03-06 22:56:55 +08:00
for line in contents.lines() {
2017-04-18 14:55:09 +08:00
if line.contains(query) {
2017-03-06 22:56:55 +08:00
// do something with line
}
}
}
</code></pre>
2017-04-18 14:55:09 +08:00
<p><span class="caption">Listing 12-18: Adding functionality to see if the line
contains the string in <code>query</code></span></p>
2017-03-06 22:56:55 +08:00
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
2017-04-18 14:55:09 +08:00
<a class="header" href="#存储匹配的行" name="存储匹配的行"><h4>存储匹配的行</h4></a>
<p>最后我们需要一个方法来存储包含查询字符串的行。为此可以在<code>for</code>循环之前创建一个可变的 vector 并调用<code>push</code>方法在 vector 中存放一个<code>line</code>。在<code>for</code>循环之后,返回这个 vector如列表 12-19 所示:</p>
2017-03-27 16:17:20 +08:00
<p><span class="filename">Filename: src/lib.rs</span></p>
2017-04-18 14:55:09 +08:00
<pre><code class="language-rust,ignore">fn search&lt;'a&gt;(query: &amp;str, contents: &amp;'a str) -&gt; Vec&lt;&amp;'a str&gt; {
2017-03-06 22:56:55 +08:00
let mut results = Vec::new();
for line in contents.lines() {
2017-04-18 14:55:09 +08:00
if line.contains(query) {
2017-03-06 22:56:55 +08:00
results.push(line);
}
}
results
}
</code></pre>
2017-04-18 14:55:09 +08:00
<p><span class="caption">Listing 12-19: Storing the lines that match so that we
can return them</span></p>
2017-03-06 22:56:55 +08:00
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
2017-04-18 14:55:09 +08:00
<p>现在<code>search</code>函数应该返回只包含<code>query</code>的那些行,而测试应该会通过。让我们运行测试:</p>
2017-03-06 22:56:55 +08:00
<pre><code>$ cargo test
running 1 test
test test::one_result ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Running target/debug/greprs-2f55ee8cd1721808
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
Doc-tests greprs
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
</code></pre>
2017-04-18 14:55:09 +08:00
<p>测试通过了,很好,它可以工作了!</p>
<p>现在测试通过了,我们可以考虑一下重构<code>search</code>的实现并时刻保持测试通过来保持其功能不变的机会了。这些代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并深入探索迭代器并看看如何改进代码。</p>
<!-- If we aren't going into this here, maybe just keep it focused, there's a
lot going on here as is -->
<!-- The reason we mention refactoring here is that it's a key step in the TDD
method that we were implicitly using before. Now that we've added text to the
beginning of this section to explicitly mention that we're doing TDD and what
the steps are, we want to address the "refactor" step. People who have some
experience with Rust might also look at this example and wonder why we're not
doing this in a different way, and be concerned that we're not teaching the
best way possible. This paragraph reassures them that we know what we're doing
and we're getting to the better way in Chapter 13. /Carol -->
<a class="header" href="#在run函数中使用search函数" name="在run函数中使用search函数"><h4><code>run</code>函数中使用<code>search</code>函数</h4></a>
<p>现在<code>search</code>函数是可以工作并测试通过了的,我们需要实际在<code>run</code>函数中调用<code>search</code>。需要将<code>config.query</code>值和<code>run</code>从文件中读取的<code>contents</code>传递给<code>search</code>函数。接着<code>run</code>会打印出<code>search</code>返回的每一行:</p>
2017-03-06 22:56:55 +08:00
<p><span class="filename">Filename: src/lib.rs</span></p>
<pre><code class="language-rust,ignore">pub fn run(config: Config) -&gt; Result&lt;(), Box&lt;Error&gt;&gt; {
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&amp;mut contents)?;
2017-04-18 14:55:09 +08:00
for line in search(&amp;config.query, &amp;contents) {
2017-03-06 22:56:55 +08:00
println!(&quot;{}&quot;, line);
}
Ok(())
}
</code></pre>
<!-- Will add ghosting and wingdings in libreoffice /Carol -->
2017-04-18 14:55:09 +08:00
<p>这里再一次使用了<code>for</code>循环获取了<code>search</code>返回的每一行,而对每一行运行的代码将他们打印了出来。</p>
<p>现在整个程序应该可以工作了!让我们试一试,首先使用一个只会在艾米莉·狄金森的诗中返回一行的单词 &quot;frog&quot;</p>
<pre><code>$ cargo run frog poem.txt
2017-03-06 22:56:55 +08:00
Compiling greprs v0.1.0 (file:///projects/greprs)
Finished debug [unoptimized + debuginfo] target(s) in 0.38 secs
2017-04-18 14:55:09 +08:00
Running `target/debug/greprs frog poem.txt`
2017-03-06 22:56:55 +08:00
How public, like a frog
2017-04-18 14:55:09 +08:00
</code></pre>
<p>好的!接下来,像 &quot;the&quot; 这样会匹配多行的单词会怎么样呢:</p>
<pre><code>$ cargo run the poem.txt
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/greprs the poem.txt`
Then there's a pair of us — don't tell!
2017-03-06 22:56:55 +08:00
To tell your name the livelong day
</code></pre>
2017-04-18 14:55:09 +08:00
<p>最后,让我们确保搜索一个在诗中哪里都没有的单词时不会得到任何行,比如 &quot;monomorphization&quot;</p>
<pre><code>$ cargo run monomorphization poem.txt
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/greprs monomorphization poem.txt`
</code></pre>
<p>非常好!我们创建了一个属于自己的经典工具,并学习了很多如何组织程序的知识。我们还学习了一些文件输入输出、生命周期、测试和命令行解析的内容。</p>
<p>现在如果你希望的话请随意移动到第十三章。为了使这个项目章节更丰满,我们将简要的展示如何处理环境变量和打印到标准错误,这两者在编写命令行程序时都很有用。</p>
2017-03-06 22:56:55 +08:00
2017-03-02 23:36:03 +08:00
</div>
<!-- Mobile navigation buttons -->
<a href="ch12-03-improving-error-handling-and-modularity.html" class="mobile-nav-chapters previous">
<i class="fa fa-angle-left"></i>
</a>
<a href="ch12-05-working-with-environment-variables.html" class="mobile-nav-chapters next">
<i class="fa fa-angle-right"></i>
</a>
</div>
<a href="ch12-03-improving-error-handling-and-modularity.html" class="nav-chapters previous" title="You can navigate through the chapters using the arrow keys">
<i class="fa fa-angle-left"></i>
</a>
<a href="ch12-05-working-with-environment-variables.html" class="nav-chapters next" title="You can navigate through the chapters using the arrow keys">
<i class="fa fa-angle-right"></i>
</a>
</div>
<!-- Local fallback for Font Awesome -->
<script>
if ($(".fa").css("font-family") !== "FontAwesome") {
$('<link rel="stylesheet" type="text/css" href="_FontAwesome/css/font-awesome.css">').prependTo('head');
}
</script>
<!-- Livereload script (if served using the cli tool) -->
<script src="highlight.js"></script>
<script src="book.js"></script>
</body>
</html>