100% found this document useful (2 votes)
75 views40 pages

Hands-On Test-Driven Development: Using Ruby, Ruby On Rails, and RSpec 1st Edition Greg Donald All Chapter Instant Download

RSpec

Uploaded by

waldoanimaej
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
100% found this document useful (2 votes)
75 views40 pages

Hands-On Test-Driven Development: Using Ruby, Ruby On Rails, and RSpec 1st Edition Greg Donald All Chapter Instant Download

RSpec

Uploaded by

waldoanimaej
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 40

Get ebook downloads in full at ebookmeta.

com

Hands-on Test-Driven Development: Using Ruby, Ruby


on Rails, and RSpec 1st Edition Greg Donald

https://ebookmeta.com/product/hands-on-test-driven-
development-using-ruby-ruby-on-rails-and-rspec-1st-edition-
greg-donald/

OR CLICK BUTTON

DOWNLOAD NOW

Explore and download more ebook at https://ebookmeta.com


Recommended digital products (PDF, EPUB, MOBI) that
you can download immediately if you are interested.

Hands-on Test-Driven Development: Using Ruby, Ruby on


Rails, and RSpec 1 / converted Edition Greg Donald

https://ebookmeta.com/product/hands-on-test-driven-development-using-
ruby-ruby-on-rails-and-rspec-1-converted-edition-greg-donald/

ebookmeta.com

Learn Rails 5.2: Accelerated Web Development with Ruby on


Rails Stefan Wintermeyer

https://ebookmeta.com/product/learn-rails-5-2-accelerated-web-
development-with-ruby-on-rails-stefan-wintermeyer/

ebookmeta.com

Ruby on Rails Tutorial 6th Ed 6th Edition Michael Hartl

https://ebookmeta.com/product/ruby-on-rails-tutorial-6th-ed-6th-
edition-michael-hartl/

ebookmeta.com

The crime of aggression under the Rome Statute of the


International Criminal Court Second Edition Carrie
Mcdougall
https://ebookmeta.com/product/the-crime-of-aggression-under-the-rome-
statute-of-the-international-criminal-court-second-edition-carrie-
mcdougall/
ebookmeta.com
Effective Python Penetration Testing 1st Edition Rejah
Rehim

https://ebookmeta.com/product/effective-python-penetration-
testing-1st-edition-rejah-rehim/

ebookmeta.com

Hero Code Star Kingdom 03 0 Lindsay Buroker Et El

https://ebookmeta.com/product/hero-code-star-kingdom-03-0-lindsay-
buroker-et-el/

ebookmeta.com

Railway Transportation Systems: Design, Construction and


Operation, 2nd Edition Christos N. Pyrgidis

https://ebookmeta.com/product/railway-transportation-systems-design-
construction-and-operation-2nd-edition-christos-n-pyrgidis/

ebookmeta.com

Creative Haven Paradise Designs Coloring Book (Creative


Haven Coloring Books) Menten

https://ebookmeta.com/product/creative-haven-paradise-designs-
coloring-book-creative-haven-coloring-books-menten/

ebookmeta.com

Immortal Lord 2nd Edition H M Mcqueen

https://ebookmeta.com/product/immortal-lord-2nd-edition-h-m-mcqueen/

ebookmeta.com
Rick Steves Belgium Bruges Brussels Antwerp Ghent Rick
Steves Gene Openshaw

https://ebookmeta.com/product/rick-steves-belgium-bruges-brussels-
antwerp-ghent-rick-steves-gene-openshaw-2/

ebookmeta.com
Hands-on
Test-Driven
Development
Using Ruby, Ruby on Rails, and RSpec

Greg Donald
Hands-on Test-Driven Development
Greg Donald

Hands-on Test-Driven
Development
Using Ruby, Ruby on Rails, and RSpec
Greg Donald
Clarksville, TN, USA

ISBN-13 (pbk): 978-1-4842-9747-6 ISBN-13 (electronic): 978-1-4842-9748-3


https://doi.org/10.1007/978-1-4842-9748-3

Copyright © 2024 by Greg Donald


This work is subject to copyright. All rights are reserved by the Publisher, whether the whole or part of the material is concerned,
specifically the rights of translation, reprinting, reuse of illustrations, recitation, broadcasting, reproduction on microfilms or in
any other physical way, and transmission or information storage and retrieval, electronic adaptation, computer software, or by
similar or dissimilar methodology now known or hereafter developed.
Trademarked names, logos, and images may appear in this book. Rather than use a trademark symbol with every occurrence of
a trademarked name, logo, or image we use the names, logos, and images only in an editorial fashion and to the benefit of the
trademark owner, with no intention of infringement of the trademark.
The use in this publication of trade names, trademarks, service marks, and similar terms, even if they are not identified as such,
is not to be taken as an expression of opinion as to whether or not they are subject to proprietary rights.
While the advice and information in this book are believed to be true and accurate at the date of publication, neither the authors
nor the editors nor the publisher can accept any legal responsibility for any errors or omissions that may be made. The publisher
makes no warranty, express or implied, with respect to the material contained herein.

Managing Director, Apress Media LLC: Welmoed Spahr


Acquisitions Editor: Melissa Duffy
Development Editor: James Markham
Coordinating Editor: Gryffin Winkler

Cover designed by eStudioCalamar


Cover image by [email protected]

Distributed to the book trade worldwide by Apress Media, LLC, 1 New York Plaza, New York, NY 10004, U.S.A. Phone
1-800-SPRINGER, fax (201) 348-4505, e-mail [email protected], or visit www.springeronline.com. Apress Media,
LLC is a California LLC and the sole member (owner) is Springer Science + Business Media Finance Inc (SSBM Finance
Inc). SSBM Finance Inc is a Delaware corporation.
For information on translations, please e-mail [email protected]; for reprint, paperback, or audio rights,
please e-mail [email protected].
Apress titles may be purchased in bulk for academic, corporate, or promotional use. eBook versions and licenses are also
available for most titles. For more information, reference our Print and eBook Bulk Sales web page at http://www.apress.com/
bulk-sales.
Any source code or other supplementary material referenced by the author in this book is available to readers on GitHub (https://
github.com/Apress). For more detailed information, please visit https://www.apress.com/gp/services/source-code.

Paper in this product is recyclable


This book is dedicated to my wife and best friend, Sunni. She
has been a constant source of support and encouragement
during the writing of this book.
Contents

1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
What Are We Building? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Our Database . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Why Another TDD Book? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
My Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Why You Should Trust Me . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Audience and Prerequisites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
How to Read This Book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2 What Is Test-Driven Development? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Problems with Late Test Writing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
No Code Design . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Passing by Accident . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Code Spikes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
The Red-Green-Refactor Development Cycle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Red Phase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Green Phase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Refactor Phase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
Wash, Rinse, and Repeat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
Advantages of Building Software Using TDD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Code Design . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
No Broken Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Maintaining Focus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Acceptance Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Code Quality . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3 Getting Started with Ruby . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Installing Homebrew . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Installing rbenv . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Installing Ruby . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
4 Getting Started with Ruby on Rails . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Installing Bundler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Installing Ruby on Rails . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Installing PostgreSQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Installing Node.js . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

vii
viii Contents

Creating a New Ruby on Rails Application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20


Running Rails Locally . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
5 Setting Up RSpec and FactoryBot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
Installing RSpec . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
Installing FactoryBot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
6 Adding Initial Models . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
The User Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
The User Factory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
How to Run Specs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
User Model Validations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
The Page Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
RSpec’s “subject” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
FactoryBot “build” vs. “create” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
The Page slug . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
Page Factory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
Final Thoughts on Model Validations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
User Spec Cleanup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Advice on Writing Too Many Specs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
7 Creating Pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
8 Build Homepage Contents . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
View Specs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
Shared Partial Template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
More View Specs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
Homepage Layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
CSS Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Layout Template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Sidebar Layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
9 Sidebar Contents . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
Search As a Service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
Implementing a Search Service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
Searching by Term . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
Searching for Pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
Adding a Search Form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
Displaying Search Results . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
Pages Archive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
Creating a List of Months and Years . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
Archive List View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
Archive Integration Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
Contents ix

10 Page Tags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105


Adding Page Tags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
Creating the Tag Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
Creating the Page Tag Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
Jumping Over Relationships . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
Tagging Pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
After Save Callbacks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
Display Tags on Pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
Handling Missing Tags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
Refactor Duplicate Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
11 Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
Active Storage for Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
Getting Started with Active Storage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
Adding an Image Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
Serving Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
Adding an Image Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
12 User Authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
Improving Our User Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
Password Hashing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
Adding Fields to Our User Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
Add a Hashing Library . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
Authenticating a User . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
Improving Authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
Password Confirmation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
Skipping Password Validation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
13 Administration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
Adding Active Admin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
Adding the Gem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
Testing Active Admin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
Logging In . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
Admin Layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
Managing Pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
Overriding Active Admin Form Field Values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
Factoring Out Duplicate Login Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
Previewing a Page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
Managing Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
Viewing Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
Uploading New Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
Editing Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
Deleting Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
Admin Filters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
Managing Users . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
Adding New Users . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
x Contents

Deleting Users . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185


Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
14 Odds and Ends . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
Build a Sitemap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
Pagination . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
Fixing Specs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198
Styling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
CSS in Rails . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
Styling Our Public Pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
Styling Our Admin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
Wrapping Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205
Code Highlighting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205
Stimulus and Turbo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
15 Bonus: Deploy to Production . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
Why This Non-cloud Approach? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
Acquiring a Production Linux Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
Getting Production Ready . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
Installing Dependencies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
Installing Ruby . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
Installing Phusion Passenger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
Configuring PostgreSQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
Storing Code on GitHub . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
Installing Capistrano . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
Configuring Capistrano . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
Deploying Our Rails Application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
Configuring Our Apache Virtual Host . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
Logging In . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225

Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
About the Author

Greg Donald has been a professional software engineer since


1996 and has worked with Ruby on Rails since 2006. Prior
to that, he worked on many Perl and PHP projects, finding
the applications messy and hard to maintain. He became a test
driven development enthusiasts after having seen the results of
software written in various forms, and determining no style was
more successful than TDD. He decided to write this book to
promote TDD and share his positive experiences using it.

xi
About the Technical Reviewer

Eldon is a senior technologist, creator, and United States Marine Corps veteran. He’s the author
of two software development books. His career has allowed him to work across a wide variety of
technologies and domains. His career has taken him from working in successful startups in roles
ranging from senior developer to chief architect to working as a principal technologist for a global
software consulting firm, helping enterprises and companies rescue software projects and/or evolve
their software delivery capabilities.
These days, he works as a technical fellow with a mission of raising the bar of software
development globally for a technology company providing tools, AI, and research to advance the
rule of law.
Currently, he lives in Florida where he enjoys scuba diving and creating video content.

xiii
Introduction
1

Hello, and welcome! I’m very excited to share with you my book about test-driven software
development. I’ve been a software engineer for over 25 years, and I’ve seen the results of software
written using many different styles and techniques. It wasn’t so long ago that we didn’t even write tests
for our software. At some point, some very smart people realized that writing tests for our software
was actually a good idea. Our tests would provide us with confidence that our software worked as
expected and even more importantly keep us from getting phone calls in the middle of the night to fix
things that would break in production.
Not long after we started writing tests, some other very smart people realized that writing our tests
first would give us an opportunity to think about what we were going to write before we would write
it. It would give us a target to aim at, in effect. This was the birth of test-driven development (TDD).
TDD is a style of software development where we write our tests first, see that they are failing as
expected, and only then do we pursue writing the code to make those tests pass.
This is the central idea of my book. We will think about what we want to do, then we will capture
that idea with a properly failing test. We will then write the code to make the test pass. We will then
refactor our code to make it better, or prettier, or less repetitive, and then we will write another test.
We will repeat this process over and over until we have a fully functional web application.
Let’s get started!

What Are We Building?

As you pick up this book and look inside at the first few pages, you may be wondering, what exactly
are we going to build? I’m so glad you asked! We will build a fully functional blog! A “blog,” short
for “weblog” or “web log,” is a web application that allows its administrator (you) to post articles to
the Web. Instead of downloading premade blog software, for example, WordPress,1 we’re going to
build our own blog software from scratch.
We’ll be using the Ruby2 programming language and the Ruby on Rails3 web framework to build
everything. These are the best tools for the job, in my opinion. Ruby is a very expressive and powerful
programming language, and Ruby on Rails is a very powerful web framework.
1 https://wordpress.org/
2 https://ruby-lang.org/
3 https://rubyonrails.org/

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2024 1
G. Donald, Hands-on Test-Driven Development,
https://doi.org/10.1007/978-1-4842-9748-3_1
2 1 Introduction

Figure 1-1 Blog database diagram

We’ll use the test-driven development (TDD) style of software design and engineering, which
means we’ll write failing tests first and only then write our implementation code to get our failing
tests passing.
We’ll use the RSpec4 testing framework to write our tests. RSpec describes itself as a tool for
performing “behavior-driven development” and is in fact the best tool available for testing Ruby
code. If you’ve never used RSpec before, don’t worry; in this book, I’ll cover more than enough to
get you started and productive.
Our blog will be a simple web application overall, but will be complex enough to demonstrate the
test-driven development style of software development. Our blog will allow us to create and update
pages as well as search them by keyword and tag them for easy categorization.
Let’s take a look at our database design next.

Our Database

We will use the PostgreSQL5 relational database management system (RDBMS) to store our blog’s
data. When it comes to free and open source databases, PostgreSQL is best in its class.
At the end of the book, our database design will look similar to the diagram in Figure 1-1.

4 https://rspec.info/
5 https://postgresql.org/
Why Another TDD Book? 3

We’ll go into much more detail about our database design in later chapters, but I want to give you
an initial idea of what we’ll be building early on. We will have database tables for users, pages, tags,
and images. Our image table is not connected to any of the other previously mentioned tables. Our tag
table is connected to our page table through our page_tags join table. Each of our pages will belong
to a single user. With this design, we can have more than one user, and each user can be an author of
many pages.
We will build up our database design as we go, adding new tables and columns as we need them,
as that’s the nature of practicing proper test-driven development.

Why Another TDD Book?

The main goal of this book is to provide a practical, hands-on introduction to the test-driven style of
software design and engineering.
Many books on the topic of test-driven development have been written before this one. Some of
them are actually very good, but none of them were written in the practical, hands-on style of this
book. In fact, very few books are written in this style.
This book covers the building of an entire web application, from start to finish. Our focus
application, a blog, is the most practical thing I could think to build, given my audience of software
engineering readers. Every software engineer has their own online blog, right? A blog is simple
enough that it can be covered in a single book, but complex enough that it can be used to demonstrate
my favorite parts of RSpec while building a Ruby on Rails web application.
Most other TDD books are written in a small-problem sort of writing style. They cover single
topics, one at a time, barely more than a reference guide. They do not cover the entire process of
building a software application from start to finish, using TDD. As a result, they are, in my opinion,
less practical and much less useful for actually learning practical TDD.

My Motivation

About five years ago, I joined a team of software engineers who were busy working on a very
important (people’s lives were at stake) large-scale web application. They were several months into
it at the time, and there were deadlines. The application was being written in Ruby on Rails, and the
team was using TDD to build it. They were practicing Extreme Programming (XP),6 as described
by Kent Beck,7 pretty much to the letter. I had heard of TDD and XP before, but had never actually
practiced them. I had never even pair-programmed8 an entire day prior to joining the team. I was
eager to learn, and little did I know at the time that I was working with some of the best software
engineers I had ever met.
These girls and guys were amazingly agile. They refactored their code constantly. They were
always looking for ways to reduce code complexity and improve their processes. They
were constantly learning new things, and they were always teaching each other new things. They
were very passionate about their craft and passionate about leveling up their team even more so.
I soon realized I was in over my head. I was a n00b to the team, to TDD, and to XP. They did not
seem to care about any of that.

6 www.extremeprogramming.org/
7 http://wiki.c2.com/?KentBeck
8 http://wiki.c2.com/?PairProgramming
4 1 Introduction

They were very patient with me, and they were completely willing to teach me everything they
knew. They taught me how to practice proper TDD and XP. They taught me how to write failing tests
first and how to write code that is easy to test. They taught me how to write tests that supplied courage
for when I would later perform fearless refactoring.
Over the next couple of years, I became a much better software engineer. The first major change I
noticed in myself was how I had stopped worrying about code I had recently pushed into production.
I was no longer thinking about code breaking in production during my off hours or how these sorts
of breakages were often followed by an emergency phone call to hurry to fix things. I had stopped
worrying about code breaking because I knew my code was completely covered with well-written
tests and very unlikely to break.
Another change I noticed in myself was that I was writing cleaner code. I was writing code that
was easier to later understand. I was only writing the code that was necessary to get my tests into a
passing state. I got into a pattern of knowing that once my tests pass, my implementation work was
done. Moving on to refactor any ugliness or code duplication is my only next step before writing my
next failing test.
I’m telling you all of this because I want you to understand how I’ve come to learn and respect
test-driven development. I learned it by doing it, for many years, with a team of experts. I know that
developing software using TDD produces better software than not using TDD to develop software. I
know this because I have seen the final results with my own eyes.

Why You Should Trust Me

I’ve been a computer programmer for a long time. If we’re counting uncompensated work, then I’ve
been programming since 1984, when I wrote my first bits of BASIC9 code on my grandpa’s TI-
99/4A,10 saving my work to a cassette tape. I went on to write more advanced programs and games
in BASIC at my junior high school, where we had Apple IIe11 computers in our computer lab. A few
years later, in high school, we had IBM PC clones12 in our computer lab, and it was at this time I
learned to program in Turbo Pascal.13
I joined the US Navy in 1990 and didn’t have much contact with computers for the next few years.
When I got out in 1995, I discovered the World Wide Web was a thing, and I became instantly hooked
on my newly found favorite hobby, building web pages. I taught myself HTML and enough Perl14
to be dangerous, and by late 1996, I had landed my first paid programming job as a web developer
working on Perl-based shopping cart software.
So if we’re counting compensated work, I’ve been a self-taught professional software engineer
since late 1996. I’ve written code in almost every major programming language and on many different
operating systems and platforms. My focus has been on web development for the most part, but I
also have significant experience working on mobile applications and games, in native code, for both
Android and iOS. I was an early publisher on Google’s Android platform, having the first published
Blackjack game there.

9 http://hopl.info/showlanguage.prx?exp=176
10 www.ti994.com/
11 www.old-computers.com/museum/computer.asp?st=1&c=83
12 www.ibm.com/ibm/history/exhibits/pc/pc_1.html
13 http://progopedia.com/implementation/turbo-pascal/
14 www.perl.org/
Other documents randomly have
different content
"No."

Grimes looked at the key critically. "H'm! A spring lock. Do you mind
opening this drawer?"

"Why should I open it? It's my private drawer." Betty thought of her
Marcus Aurelius and Bob's precious letter. Why should these sacred things
be dragged out by this vulgar detective?

"Oh, it's your private drawer, is it? Just the same, I must ask you to open
it, Miss Thompson."

"Very well," yielded the girl. "There!" She put the key in the lock and
turned it while Grimes watched her keenly.

"Now if your lordship will look in this drawer?" he said.

"Certainly," bowed the prelate, and he pulled out the drawer to its full
length, then started back with a cry of amazement. "Good heavens!" He
drew forth a bundle of folded banknotes. "It's the stolen money," he
declared. "The exact amount! The identical notes! Five thousand pounds!"

Betty started in bewilderment. "But—I don't understand," she said.

Old Bunchester turned to the girl in deep concern. "My dear Miss
Thompson, this is exceedingly painful, exceedingly compromising. I beg
you most earnestly, in the interest of everyone, in your own interest, to tell
us how it comes that this money is found in your desk. You must explain
this mystery, indeed you must."

"Hold on!" cried Bob, springing forward, his whole face transfigured,
and here it was, in the words of Hiram Baxter, that the boy showed himself
a thoroughbred and took the five-bar gate in one clean leap. "Don't say a
word, Betty. Don't explain anything. You're the finest, pluckiest girl I ever
knew, and right now, without any explanation, I ask you to be my wife."

"Bob!" she cried, and her whole soul was in her eyes.
"It's all right, dear." He stood close beside her and drew her to him
protectingly. "There are two of us now." Then, turning to Grimes: "Go
ahead with your silly little game."

"All very pretty," sniffed the detective, while the bishop looked on in
purple amazement, "but, before we get through with our silly little game
you may not find it as silly as you think."

He strode across the library to the foot of the little stair and pointed to
the mezzanine door. "If Miss Thompson was so confident that Jenny Regan
was a deserving person why did she hide her in that room this morning?"

"What?" cried Bob.

Grimes fixed his hard gaze on Betty. "Do you deny that you hid Hester
Storm, otherwise known as Jenny Regan, in that room?"

The girl eyed him steadily. "It's true," she said; "but—I can explain it."

Young Baxter started to his feet. "It isn't possible this Storm girl who's
been working here is—Jenny Regan?"

Grimes nodded. "Jenny Regan is one of her aliases. It's a matter of


police record. You knew this, didn't you?" He turned to Betty, whose cheeks
were aflame with anger.

"Yes, I knew it," she flung back, "and what is more——"

"You knew she was a thief and a pickpocket?" he added.

With an effort the girl checked herself and stood panting.

"If your lordship will give me a few moments," she said in a low tone,
"I can make everything clear. You don't mind, Bob? Just a few moments?"

Baxter bowed to her wish. "Of course I don't mind. Come on," he said
to Grimes.
"Not I," refused the latter. "Miss Thompson says she can make things
clear to his lordship. So can I. His lordship's purse was stolen by Hester
Storm, alias Jenny Regan, but this young woman," he swept Betty with a
cruel look, "was an accessory after the fact."

"You miserable hound!" roared Bob.

And the bishop said solemnly: "My dear sir, you are making an
incredible accusation. Miss Thompson is a lady—a friend of mine. I knew
her estimable father."

"I can only lay the facts before your lordship," shrugged the detective.
He went to the library door, and, motioning quickly, returned followed by
Hester Storm, who looked neither to the right nor the left, but held her eyes
straight down before her, as if studying the yellowish pattern in the carpet.
Betty watched her in surprise.

"There," Grimes pointed to Hester, "is my answer to your lordship's


doubts. What is this woman doing here? She is a notorious thief and a
pickpocket. Why did she come to Ipping House? Why did your lordship's
friend, Miss Thompson, shelter her in that bedroom and try to prevent me
from arresting her? The answer is easy. It was because Miss Thompson
proposed to share the money this Storm girl had stolen from your lordship."

"That's a lie!" rang out Betty's swift denial. "Tell them it's a lie. You
must tell them," she appealed frantically to Hester.

But the Storm girl never moved; she never spoke; she never lifted her
eyes from the carpet.

And Grimes went on relentlessly: "If Miss Thompson was innocent of


this crime why did she not tell the whole truth about it when she was alone
with your lordship not half an hour ago?"

"I wanted to tell the truth," insisted Betty, "but I had promised this poor
girl that I would do nothing until—until the detective had gone." Again she
appealed to Hester. "You know that is true. Tell them it's true."
But the Storm girl stood there like a frozen image, her lips closed, her
eyes cast down. And a sickening terror filled Betty's breast.

"Your lordship must see that there is a strong case against this young
woman." Grimes moved toward Betty with a grim tightening of the lips.
"You'll have to come with me." He laid a hand on her arm.

Instantly Bob Baxter stepped forward, his face as white as Betty's.

"Take your hands off that lady."

"Oh, I don't know," retorted Grimes. "I'm an officer of the law and——"

"My dear Mr. Baxter," reasoned the bishop, interposing his portly and
venerable presence between the excited adversaries, "believe me, we must
respect the majesty of the law."

"Majesty nothing," stormed Bob. "I tell you——"

"I tell you to step back," ordered the detective. "And you——" he faced
Miss Thompson, "consider yourself under arrest. If you have anything to
get ready you'd better do it. We start in——" he glanced at his watch, "in
ten minutes."

"Start?" cried Baxter, aghast.

The seriousness of the situation was now clear to everyone.

"See here," the young man appealed to Grimes after a moment's


thought, "there's some horrible mistake. Miss Thompson had nothing to do
with stealing that money. She couldn't steal. Look at her, man! You know
she couldn't. I'll be responsible anyway, or my father will, for the money
and everything else. You can't drag her off like this and disgrace her. By
God, I won't let you."

"I'm sorry, sir, but I've no choice. A crime has been committed, and—
there's evidence enough to hold her on if she was a cousin of the queen."
"Under arrest!" murmured Betty twining her fingers together piteously
and fixing her eyes on Hester.

At this moment the sound of carriage wheels was heard outside. Bob
went quickly to the window.

"It's Father," he said with a movement of relief. "Cheer up, Betty. Dad
will think of something."

A moment later Hiram Baxter entered the room. His face was ashen
gray. He looked broken and ill, but a flicker of the old bright smile spread
over his rugged face as he glanced about the room.

"Hello, everybody! Why, hello, Bish!" He tapped Bunchester playfully


on the shoulder. "I'm awful glad to see you, Bish." Then, as he noticed the
universal gloom, "Say, it strikes me you folks are a little frappay. What's
wrong? What are you doing here?" he asked Grimes.

The detective started to explain, but Bob cut in eagerly.

"One moment! Father, did you leave twenty-five thousand dollars in the
drawer of that desk?"

"Twenty-five thousand dollars! Say, boy, is this a joke? If it is, I tell ye


straight I don't like it."

"No, Father, it's not a joke; it's very far from a joke. Did you leave it
there?"

"Twenty-five thousand dollars in that desk? Say, if you knew what I've
been through to-day! I've been scratchin' around down where the avenues
are paved with red-hot bricks, lookin' for twenty-five thousand dollars. And
I didn't find it, either. No, sir, I left no money in that desk. It ain't my desk,
anyway; it's Betty's desk."

"Ah!" smiled Grimes.

"Say, who are you, anyway?"


"I'm Grimes from Scotland Yard."

"Let me explain," put in Betty. "I—I'm in great trouble, Guardy."

"I'll tell him, dear," said Bob. "Father, I—I've asked Betty to be my
wife."

"Well, it ain't that that's makin' ye look like a funeral, is it?" drawled
Hiram. "Go on, now; let me have it."

Betty and Bob spoke at the same time, both pointing scornful fingers at
Grimes.

"He says that I——"

"He dares to say that Betty——"

"Easy now! Not all at once. Say, Bish, you'd better tell it."

Bunchester coughed impressively. "My dear friend, it seems incredible,


but the fact is Mr. Grimes thinks that Miss Thompson was concerned in the
—er—misappropriation of that five thousand pounds."

"That was stolen from you? Betty Thompson? No, no, no!" thundered
the old man.

"That is how we all feel, but, with the utmost regret I am forced to bear
witness that this exact sum and, I believe, the identical banknotes were
found in Miss Thompson's desk—there."

"Five thousand pounds? What does this mean, Betty? How did that
money get in your desk?"

"I—I don't know," the unhappy girl answered. Grimes looked at his
watch again. "No use of any more talk," he said gruffly. "It's time to start
and——" motioning to Betty, "you'll have to come with me."

"You don't mean——" Hiram's eyes burned savagely.


"I mean that these two women are under arrest, sir, charged with grand
larceny, and I'm going to take 'em to London by the next train."

"But—I won't have it."

"Better not interfere, sir. I've men outside to help me, and—I'm going to
take 'em. Come now." He caught Betty by the arm and marched her, half
fainting, toward the door.

At this moment Hester Storm lifted her eyes, opened her lips, and spoke
in a strange, low tone:

"Wait! You mustn't take her. She didn't steal the money. She had nothing
to do with it. I stole the money. I put it in that desk. I'm the one to take."

"Hester!" cried Betty. "You—you put that money in my desk?" repeated


Betty slowly.

"Yes. I meant to steal it or—I meant to steal half of it, but—when you
sang that song about—her promise true, why—I thought how you'd been
good to me, and—trusted me, and—I sneaked in here and left the money.
The drawer was open, and I snapped it shut. Then, when I made my
getaway he pinched me." She turned to Grimes.

The detective lowered his head as if he was studying the girl through his
eyebrows.

"You told me a different story just now?" he said.

"Sure I did. I lied. You know I lied. You don't think I'm stuck on gettin'
sent away for ten years, do ye? But if it's got to be her or me, well, I won't
have her sent away when all she's done is to treat me right and try to save
me. You can take that from Hester Storm."

"This is a rare and beautiful instance of gratitude and devotion,"


commented Bunchester.

"That's all right, Bish; but I want to know more about this." Hiram
turned to Hester, who was standing with bowed head and clasped hands.
"Well, fer a girl who talks about stealin'—I guess some o' the honest folks
could take lessons from you. Say, I didn't quite get that about how you
planned to steal half o' this money? Where did the half come in? Why didn't
ye plan to steal all of it?"

Then, little by little, with questions from Grimes and more questions
from Hiram the Storm girl told her story, sometimes in broken words, as her
feelings overpowered her, but in the main simply and bravely and truthfully,
as one who is strengthened by some higher power. She went back to her
childhood and spoke of her sister Rosalie. She told of her wanderings and
waywardness, then of her visit to Ippingford and her meeting with Horatio
Merle. Then, finally, of her efforts to return the money and of the
persecution she had suffered at the hands of Anton. She kept nothing back,
and she made no excuse for herself. She had sinned and it was right that she
should suffer.

As Hester finished her confession every heart went out to her in genuine
sympathy, and Grimes was seen to wipe his eyes.

"I want to say," he remarked, "that I've seen some strange cases in my
time, but when it comes to a woman trying to steal money over again that
she's stolen once so as to give it back—why, that's a new one on me."

"Ye can't ever tell what a woman's goin' to do," nodded Baxter.

"Anyway, I owe you an apology, Miss Thompson," the detective went


on, and there was a little catch in his voice as he met Betty's grave, beautiful
eyes. "Things certainly did look black against you, but—all I can say is, I'm
sorry, Miss, I'm sorry."

"It's all right, old man," said Bob.

Whereupon the Bishop of Bunchester, clearing his throat ponderously,


addressed these comforting words to Hester Storm: "My dear young friend,
I am inexpressively touched by this story of your struggles and temptations
and your splendid moral victory. It is a most meritorious case and one that
the Society of Progressive Mothers will take up with enthusiasm. As for the
outcome of this affair, speaking for the Progressive Mothers' Society and
for myself, as bishop of this diocese, I can assure you that there will be no
unpleasant consequences, so far as you are concerned. The money has been
returned. You have truly repented of your sin and you have given an
illustration of spiritual regeneration that will long be treasured in the annals
of the Progressive Mothers' Society.

"And now, my dear Miss Thompson, how shall I express my great joy
——" The bishop turned to Betty, and was about to launch forth into
another sounding period when Hiram Baxter interrupted him.

"Excuse me, Bish, fer breakin' in on yer speech, but—I've had a bad day
in town, and—if you don't mind takin' the detective into the next room and
finishin' up the details of this purse business with him, why——"

Baxter leaned back in his chair with signs of physical distress—"ye see,
I'm just about all in."

"Why, certainly, my dear friend. Let us come in here." And, motioning


to Grimes and Hester, he led the way into the conservatory and carefully
closed the door behind him.

"Father! Is anything wrong?" asked Bob in concern.

"Guardy, you're ill?"

With anxious faces the young lovers stood beside the old man, who
smiled at them wearily.

"Children, I've got bad news fer ye, awful bad news for ye," he said.
"I've made the best fight I could, but that Henderson bunch, they've done
me up. Independent Copper broke twenty points to-day in the New York
market, and—I was long of the stock. My man cabled me the tip to sell, but
I never got it. I never got it. That cable was held up." He bent forward,
resting his big grizzled head on his hands in an attitude of utter despair. "It's
all off, children. It's all off."

Betty's heart was pounding violently as she listened. Things had


happened so rapidly in the last few hours that she had scarcely thought of
Lionel and his wild sprint for the cable office. Had he failed to get there in
time? Had he made some mistake? What could have happened to Lionel?

"Excuse me a moment," she said, and hurrying toward the conservatory,


she threw open the door and looked about her.

One glance showed that something had happened, for her eyes fell on a
murmuring group gathered about Anton and the detective. And there in the
group, calmly smoking a cigarette, was Lionel Fitz-Brown.

"Lionel!" Betty called, addressing him by his Christian name for the
first time in her life. "Please come here—quick." And then, when he stood
before her, very indignantly: "The idea of your not coming to tell me!"

"Tell you about what?" he asked blankly.

"About the cable. Did you—were you in time?"

Fitz-Brown adjusted his monocle with great care, then, gradually, a


smile spread over his face. "Oh, I say! The cable! You see, I got so beastly
wet in the storm, Miss Thompson, that I—well, the fact is, I had on thin
flannel trousers and they jolly well shrunk up to my knees and—haw, haw,
haw!" He exploded into uproarious merriment.

"Oh, Mr. Fitz-Brown," she wrung her hands beseechingly, "please tell
me if you got the cable off by twelve?"

Lionel laid a reflective forefinger along his nose. "By twelve? No. No, I
didn't."

"You didn't?" Betty's heart sank.

"I go it off five minutes before twelve. Haw, haw, haw!" He fairly
doubled up in his enjoyment of this witticism.

Like a flash, Betty darted back to Hiram, thrilling with this good news.
And at the same moment Grimes entered, holding a cablegram in his hand.
"Beg your pardon, sir," he said respectfully to Baxter, "I've just arrested
your chauffeur, Anton Busch. He's a crook, Slippery Jake, sneak thief and
confidence man, wanted by the police in half dozen cities. He's been
working some deviltry here, sir. I've just found this cablegram on him. It's
addressed to you."

"Thank you," said Hiram with a look of inexpressible sadness in his


eyes. "It's come too late."

"I'm sorry, sir. I—I'll wait outside," and Grimes withdrew, his hard face
softened by a look of deep pity for the shattered old warrior.

Baxter sat still, looking at the yellow envelope. "Too late!" he muttered.
"Oh, if I'd only got this cablegram in time!"

"Guardy, I want to tell you something," Betty began, but Hiram paid no
attention.

"Nothing matters now," he went on bitterly. "I mustn't say that. I'm
happy about you two. Betty! Bob!" He joined their hands and held them
strongly. "It's what I've always dreamed of, but—I wanted to leave ye well
fixed and now——" The tears were coursing down his grizzled cheeks.
"We're ruined—ruined."

"No, no! We're not ruined. You mustn't say that, Guardy." The girl dared
not promise anything, for she did not know the result of her effort, but she
pointed hopefully to the unopened cablegram. "Why don't you open this?
Why don't you read it?"

He shook his head despairingly. "I know what it is. It's the notice that
I've been sold out and—everything's gone. God! If I'd only known! If I
could only have given the order to sell—even a few thousand shares."

With a listless movement Hiram ripped open the cable envelope and
drew out the yellow sheet. Betty thought her heart would stop beating as
she watched his face. Slowly the look of amazement came. He rubbed his
eyes and read the message again. Then he sprang to his feet with a great cry.
"What! It ain't possible! Listen to this!" In his excitement, Hiram almost
shouted the words written there before him. "'Congratulate you on your
splendid nerve. Executed order at once. Sold fifty thousand shares at top of
market and closed out with twenty points profit. Gramercy.' You hear that,
Bob? Read it! Am I crazy or—— No, no! There's something wrong. I didn't
show any splendid nerve. I didn't cable any order to sell fifty thousand
shares. There's some mistake."

"There's no mistake," cried Betty. "I cabled the order to sell."

"You?" stared Bob.

"You?" gasped Hiram. "You cabled the order to sell fifty thousand
shares of Independent Copper stock for my account? Fifty thousand
shares?"

It was several moments before Betty could speak, and then, laughing
and crying hysterically, she told what she and Lionel had done.

"I should say it was splendid nerve," said Bob. And folding his big,
strong arms around her, "Betty, you darling!" he whispered.

She lay there happy in his arms and, looking up into his eyes with all
the fondness of her soul, answered shyly and sweetly, "Bob, my love."

And Hiram Baxter, wiping away his tears of joy, muttered to himself
(since no one else was paying any attention), "Holy cats! Is there anything a
woman won't do?"

THE END
*** END OF THE PROJECT GUTENBERG EBOOK THE BISHOP'S
PURSE ***

Updated editions will replace the previous one—the old editions will
be renamed.

Creating the works from print editions not protected by U.S.


copyright law means that no one owns a United States copyright in
these works, so the Foundation (and you!) can copy and distribute it
in the United States without permission and without paying copyright
royalties. Special rules, set forth in the General Terms of Use part of
this license, apply to copying and distributing Project Gutenberg™
electronic works to protect the PROJECT GUTENBERG™ concept
and trademark. Project Gutenberg is a registered trademark, and
may not be used if you charge for an eBook, except by following the
terms of the trademark license, including paying royalties for use of
the Project Gutenberg trademark. If you do not charge anything for
copies of this eBook, complying with the trademark license is very
easy. You may use this eBook for nearly any purpose such as
creation of derivative works, reports, performances and research.
Project Gutenberg eBooks may be modified and printed and given
away—you may do practically ANYTHING in the United States with
eBooks not protected by U.S. copyright law. Redistribution is subject
to the trademark license, especially commercial redistribution.

START: FULL LICENSE


THE FULL PROJECT GUTENBERG LICENSE
PLEASE READ THIS BEFORE YOU DISTRIBUTE OR USE THIS WORK

To protect the Project Gutenberg™ mission of promoting the free


distribution of electronic works, by using or distributing this work (or
any other work associated in any way with the phrase “Project
Gutenberg”), you agree to comply with all the terms of the Full
Project Gutenberg™ License available with this file or online at
www.gutenberg.org/license.

Section 1. General Terms of Use and


Redistributing Project Gutenberg™
electronic works
1.A. By reading or using any part of this Project Gutenberg™
electronic work, you indicate that you have read, understand, agree
to and accept all the terms of this license and intellectual property
(trademark/copyright) agreement. If you do not agree to abide by all
the terms of this agreement, you must cease using and return or
destroy all copies of Project Gutenberg™ electronic works in your
possession. If you paid a fee for obtaining a copy of or access to a
Project Gutenberg™ electronic work and you do not agree to be
bound by the terms of this agreement, you may obtain a refund from
the person or entity to whom you paid the fee as set forth in
paragraph 1.E.8.

1.B. “Project Gutenberg” is a registered trademark. It may only be


used on or associated in any way with an electronic work by people
who agree to be bound by the terms of this agreement. There are a
few things that you can do with most Project Gutenberg™ electronic
works even without complying with the full terms of this agreement.
See paragraph 1.C below. There are a lot of things you can do with
Project Gutenberg™ electronic works if you follow the terms of this
agreement and help preserve free future access to Project
Gutenberg™ electronic works. See paragraph 1.E below.
1.C. The Project Gutenberg Literary Archive Foundation (“the
Foundation” or PGLAF), owns a compilation copyright in the
collection of Project Gutenberg™ electronic works. Nearly all the
individual works in the collection are in the public domain in the
United States. If an individual work is unprotected by copyright law in
the United States and you are located in the United States, we do
not claim a right to prevent you from copying, distributing,
performing, displaying or creating derivative works based on the
work as long as all references to Project Gutenberg are removed. Of
course, we hope that you will support the Project Gutenberg™
mission of promoting free access to electronic works by freely
sharing Project Gutenberg™ works in compliance with the terms of
this agreement for keeping the Project Gutenberg™ name
associated with the work. You can easily comply with the terms of
this agreement by keeping this work in the same format with its
attached full Project Gutenberg™ License when you share it without
charge with others.

1.D. The copyright laws of the place where you are located also
govern what you can do with this work. Copyright laws in most
countries are in a constant state of change. If you are outside the
United States, check the laws of your country in addition to the terms
of this agreement before downloading, copying, displaying,
performing, distributing or creating derivative works based on this
work or any other Project Gutenberg™ work. The Foundation makes
no representations concerning the copyright status of any work in
any country other than the United States.

1.E. Unless you have removed all references to Project Gutenberg:

1.E.1. The following sentence, with active links to, or other


immediate access to, the full Project Gutenberg™ License must
appear prominently whenever any copy of a Project Gutenberg™
work (any work on which the phrase “Project Gutenberg” appears, or
with which the phrase “Project Gutenberg” is associated) is
accessed, displayed, performed, viewed, copied or distributed:
This eBook is for the use of anyone anywhere in the United
States and most other parts of the world at no cost and with
almost no restrictions whatsoever. You may copy it, give it away
or re-use it under the terms of the Project Gutenberg License
included with this eBook or online at www.gutenberg.org. If you
are not located in the United States, you will have to check the
laws of the country where you are located before using this
eBook.

1.E.2. If an individual Project Gutenberg™ electronic work is derived


from texts not protected by U.S. copyright law (does not contain a
notice indicating that it is posted with permission of the copyright
holder), the work can be copied and distributed to anyone in the
United States without paying any fees or charges. If you are
redistributing or providing access to a work with the phrase “Project
Gutenberg” associated with or appearing on the work, you must
comply either with the requirements of paragraphs 1.E.1 through
1.E.7 or obtain permission for the use of the work and the Project
Gutenberg™ trademark as set forth in paragraphs 1.E.8 or 1.E.9.

1.E.3. If an individual Project Gutenberg™ electronic work is posted


with the permission of the copyright holder, your use and distribution
must comply with both paragraphs 1.E.1 through 1.E.7 and any
additional terms imposed by the copyright holder. Additional terms
will be linked to the Project Gutenberg™ License for all works posted
with the permission of the copyright holder found at the beginning of
this work.

1.E.4. Do not unlink or detach or remove the full Project


Gutenberg™ License terms from this work, or any files containing a
part of this work or any other work associated with Project
Gutenberg™.

1.E.5. Do not copy, display, perform, distribute or redistribute this


electronic work, or any part of this electronic work, without
prominently displaying the sentence set forth in paragraph 1.E.1 with
active links or immediate access to the full terms of the Project
Gutenberg™ License.
1.E.6. You may convert to and distribute this work in any binary,
compressed, marked up, nonproprietary or proprietary form,
including any word processing or hypertext form. However, if you
provide access to or distribute copies of a Project Gutenberg™ work
in a format other than “Plain Vanilla ASCII” or other format used in
the official version posted on the official Project Gutenberg™ website
(www.gutenberg.org), you must, at no additional cost, fee or expense
to the user, provide a copy, a means of exporting a copy, or a means
of obtaining a copy upon request, of the work in its original “Plain
Vanilla ASCII” or other form. Any alternate format must include the
full Project Gutenberg™ License as specified in paragraph 1.E.1.

1.E.7. Do not charge a fee for access to, viewing, displaying,


performing, copying or distributing any Project Gutenberg™ works
unless you comply with paragraph 1.E.8 or 1.E.9.

1.E.8. You may charge a reasonable fee for copies of or providing


access to or distributing Project Gutenberg™ electronic works
provided that:

• You pay a royalty fee of 20% of the gross profits you derive from
the use of Project Gutenberg™ works calculated using the
method you already use to calculate your applicable taxes. The
fee is owed to the owner of the Project Gutenberg™ trademark,
but he has agreed to donate royalties under this paragraph to
the Project Gutenberg Literary Archive Foundation. Royalty
payments must be paid within 60 days following each date on
which you prepare (or are legally required to prepare) your
periodic tax returns. Royalty payments should be clearly marked
as such and sent to the Project Gutenberg Literary Archive
Foundation at the address specified in Section 4, “Information
about donations to the Project Gutenberg Literary Archive
Foundation.”

• You provide a full refund of any money paid by a user who


notifies you in writing (or by e-mail) within 30 days of receipt that
s/he does not agree to the terms of the full Project Gutenberg™
License. You must require such a user to return or destroy all
copies of the works possessed in a physical medium and
discontinue all use of and all access to other copies of Project
Gutenberg™ works.

• You provide, in accordance with paragraph 1.F.3, a full refund of


any money paid for a work or a replacement copy, if a defect in
the electronic work is discovered and reported to you within 90
days of receipt of the work.

• You comply with all other terms of this agreement for free
distribution of Project Gutenberg™ works.

1.E.9. If you wish to charge a fee or distribute a Project Gutenberg™


electronic work or group of works on different terms than are set
forth in this agreement, you must obtain permission in writing from
the Project Gutenberg Literary Archive Foundation, the manager of
the Project Gutenberg™ trademark. Contact the Foundation as set
forth in Section 3 below.

1.F.

1.F.1. Project Gutenberg volunteers and employees expend


considerable effort to identify, do copyright research on, transcribe
and proofread works not protected by U.S. copyright law in creating
the Project Gutenberg™ collection. Despite these efforts, Project
Gutenberg™ electronic works, and the medium on which they may
be stored, may contain “Defects,” such as, but not limited to,
incomplete, inaccurate or corrupt data, transcription errors, a
copyright or other intellectual property infringement, a defective or
damaged disk or other medium, a computer virus, or computer
codes that damage or cannot be read by your equipment.

1.F.2. LIMITED WARRANTY, DISCLAIMER OF DAMAGES - Except


for the “Right of Replacement or Refund” described in paragraph
1.F.3, the Project Gutenberg Literary Archive Foundation, the owner
of the Project Gutenberg™ trademark, and any other party
distributing a Project Gutenberg™ electronic work under this
agreement, disclaim all liability to you for damages, costs and
expenses, including legal fees. YOU AGREE THAT YOU HAVE NO
REMEDIES FOR NEGLIGENCE, STRICT LIABILITY, BREACH OF
WARRANTY OR BREACH OF CONTRACT EXCEPT THOSE
PROVIDED IN PARAGRAPH 1.F.3. YOU AGREE THAT THE
FOUNDATION, THE TRADEMARK OWNER, AND ANY
DISTRIBUTOR UNDER THIS AGREEMENT WILL NOT BE LIABLE
TO YOU FOR ACTUAL, DIRECT, INDIRECT, CONSEQUENTIAL,
PUNITIVE OR INCIDENTAL DAMAGES EVEN IF YOU GIVE
NOTICE OF THE POSSIBILITY OF SUCH DAMAGE.

1.F.3. LIMITED RIGHT OF REPLACEMENT OR REFUND - If you


discover a defect in this electronic work within 90 days of receiving it,
you can receive a refund of the money (if any) you paid for it by
sending a written explanation to the person you received the work
from. If you received the work on a physical medium, you must
return the medium with your written explanation. The person or entity
that provided you with the defective work may elect to provide a
replacement copy in lieu of a refund. If you received the work
electronically, the person or entity providing it to you may choose to
give you a second opportunity to receive the work electronically in
lieu of a refund. If the second copy is also defective, you may
demand a refund in writing without further opportunities to fix the
problem.

1.F.4. Except for the limited right of replacement or refund set forth in
paragraph 1.F.3, this work is provided to you ‘AS-IS’, WITH NO
OTHER WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO WARRANTIES OF
MERCHANTABILITY OR FITNESS FOR ANY PURPOSE.

1.F.5. Some states do not allow disclaimers of certain implied


warranties or the exclusion or limitation of certain types of damages.
If any disclaimer or limitation set forth in this agreement violates the
law of the state applicable to this agreement, the agreement shall be
interpreted to make the maximum disclaimer or limitation permitted
by the applicable state law. The invalidity or unenforceability of any
provision of this agreement shall not void the remaining provisions.

You might also like