Skip to content

Instantly share code, notes, and snippets.

@danhyun
Created April 9, 2016 02:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danhyun/7d1a223e744f43721fe15f9837a24a5e to your computer and use it in GitHub Desktop.
Save danhyun/7d1a223e744f43721fe15f9837a24a5e to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge"><![endif]-->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="Asciidoctor 1.5.4-dev">
<meta name="author" content="Dan Hyun">
<title>Testing Ratpack Applications</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700">
<style>
/*! normalize.css v2.1.2 | MIT License | git.io/normalize */
/* ========================================================================== HTML5 display definitions ========================================================================== */
/** Correct `block` display not defined in IE 8/9. */
article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary {
display: block;
}
/** Correct `inline-block` display not defined in IE 8/9. */
audio, canvas, video {
display: inline-block;
}
/** Prevent modern browsers from displaying `audio` without controls. Remove excess height in iOS 5 devices. */
audio:not([controls]) {
display: none;
height: 0;
}
/** Address `[hidden]` styling not present in IE 8/9. Hide the `template` element in IE, Safari, and Firefox < 22. */
[hidden], template {
display: none;
}
script {
display: none !important;
}
/* ========================================================================== Base ========================================================================== */
/** 1. Set default font family to sans-serif. 2. Prevent iOS text size adjust after orientation change, without disabling user zoom. */
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/** Remove default margin. */
body {
margin: 0;
}
/* ========================================================================== Links ========================================================================== */
/** Remove the gray background color from active links in IE 10. */
a {
background: transparent;
}
/** Address `outline` inconsistency between Chrome and other browsers. */
a:focus {
outline: thin dotted;
}
/** Improve readability when focused and also mouse hovered in all browsers. */
a:active, a:hover {
outline: 0;
}
/* ========================================================================== Typography ========================================================================== */
/** Address variable `h1` font-size and margin within `section` and `article` contexts in Firefox 4+, Safari 5, and Chrome. */
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/** Address styling not present in IE 8/9, Safari 5, and Chrome. */
abbr[title] {
border-bottom: 1px dotted;
}
/** Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. */
b, strong {
font-weight: bold;
}
/** Address styling not present in Safari 5 and Chrome. */
dfn {
font-style: italic;
}
/** Address differences between Firefox and other browsers. */
hr {
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}
/** Address styling not present in IE 8/9. */
mark {
background: #ff0;
color: #000;
}
/** Correct font family set oddly in Safari 5 and Chrome. */
code, kbd, pre, samp {
font-family: monospace, serif;
font-size: 1em;
}
/** Improve readability of pre-formatted text in all browsers. */
pre {
white-space: pre-wrap;
}
/** Set consistent quote types. */
q {
quotes: "\201C" "\201D" "\2018" "\2019";
}
/** Address inconsistent and variable font size in all browsers. */
small {
font-size: 80%;
}
/** Prevent `sub` and `sup` affecting `line-height` in all browsers. */
sub, sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* ========================================================================== Embedded content ========================================================================== */
/** Remove border when inside `a` element in IE 8/9. */
img {
border: 0;
}
/** Correct overflow displayed oddly in IE 9. */
svg:not(:root) {
overflow: hidden;
}
/* ========================================================================== Figures ========================================================================== */
/** Address margin not present in IE 8/9 and Safari 5. */
figure {
margin: 0;
}
/* ========================================================================== Forms ========================================================================== */
/** Define consistent border, margin, and padding. */
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/** 1. Correct `color` not being inherited in IE 8/9. 2. Remove padding so people aren't caught out if they zero out fieldsets. */
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/** 1. Correct font family not being inherited in all browsers. 2. Correct font size not being inherited in all browsers. 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. */
button, input, select, textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 2 */
margin: 0; /* 3 */
}
/** Address Firefox 4+ setting `line-height` on `input` using `!important` in the UA stylesheet. */
button, input {
line-height: normal;
}
/** Address inconsistent `text-transform` inheritance for `button` and `select`. All other form control elements do not inherit `text-transform` values. Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. Correct `select` style inheritance in Firefox 4+ and Opera. */
button, select {
text-transform: none;
}
/** 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls. 2. Correct inability to style clickable `input` types in iOS. 3. Improve usability and consistency of cursor style between image-type `input` and others. */
button, html input[type="button"], input[type="reset"], input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/** Re-set default cursor for disabled elements. */
button[disabled], html input[disabled] {
cursor: default;
}
/** 1. Address box sizing set to `content-box` in IE 8/9. 2. Remove excess padding in IE 8/9. */
input[type="checkbox"], input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/** 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome (include `-moz` to future-proof). */
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/** Remove inner padding and search cancel button in Safari 5 and Chrome on OS X. */
input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/** Remove inner padding and border in Firefox 4+. */
button::-moz-focus-inner, input::-moz-focus-inner {
border: 0;
padding: 0;
}
/** 1. Remove default vertical scrollbar in IE 8/9. 2. Improve readability and alignment in all browsers. */
textarea {
overflow: auto; /* 1 */
vertical-align: top; /* 2 */
}
/* ========================================================================== Tables ========================================================================== */
/** Remove most spacing between table cells. */
table {
border-collapse: collapse;
border-spacing: 0;
}
meta.foundation-mq-small {
font-family: "only screen and (min-width: 768px)";
width: 768px;
}
meta.foundation-mq-medium {
font-family: "only screen and (min-width:1280px)";
width: 1280px;
}
meta.foundation-mq-large {
font-family: "only screen and (min-width:1440px)";
width: 1440px;
}
*, *:before, *:after {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
html, body {
font-size: 100%;
}
body {
background: white;
color: rgba(0, 0, 0, 0.8);
padding: 0;
margin: 0;
font-family: "Noto Serif", "DejaVu Serif", serif;
font-weight: normal;
font-style: normal;
line-height: 1;
position: relative;
cursor: auto;
}
a:hover {
cursor: pointer;
}
img, object, embed {
max-width: 100%;
height: auto;
}
object, embed {
height: 100%;
}
img {
-ms-interpolation-mode: bicubic;
}
#map_canvas img, #map_canvas embed, #map_canvas object, .map_canvas img, .map_canvas embed, .map_canvas object {
max-width: none !important;
}
.left {
float: left !important;
}
.right {
float: right !important;
}
.text-left {
text-align: left !important;
}
.text-right {
text-align: right !important;
}
.text-center {
text-align: center !important;
}
.text-justify {
text-align: justify !important;
}
.hide {
display: none;
}
.antialiased, body {
-webkit-font-smoothing: antialiased;
}
img {
display: inline-block;
vertical-align: middle;
}
textarea {
height: auto;
min-height: 50px;
}
select {
width: 100%;
}
p.lead, .paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p {
font-size: 1.21875em;
line-height: 1.6;
}
.subheader, .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title {
line-height: 1.45;
color: #7a2518;
font-weight: normal;
margin-top: 0;
margin-bottom: 0.25em;
}
/* Typography resets */
div, dl, dt, dd, ul, ol, li, h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6, pre, form, p, blockquote, th, td {
margin: 0;
padding: 0;
direction: ltr;
}
/* Default Link Styles */
a {
color: #2156a5;
text-decoration: underline;
line-height: inherit;
}
a:hover, a:focus {
color: #1d4b8f;
}
a img {
border: none;
}
/* Default paragraph styles */
p {
font-family: inherit;
font-weight: normal;
font-size: 1em;
line-height: 1.6;
margin-bottom: 1.25em;
text-rendering: optimizeLegibility;
}
p aside {
font-size: 0.875em;
line-height: 1.35;
font-style: italic;
}
/* Default header styles */
h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 {
font-family: "Open Sans", "DejaVu Sans", sans-serif;
font-weight: 300;
font-style: normal;
color: #ba3925;
text-rendering: optimizeLegibility;
margin-top: 1em;
margin-bottom: 0.5em;
line-height: 1.0125em;
}
h1 small, h2 small, h3 small, #toctitle small, .sidebarblock > .content > .title small, h4 small, h5 small, h6 small {
font-size: 60%;
color: #e99b8f;
line-height: 0;
}
h1 {
font-size: 2.125em;
}
h2 {
font-size: 1.6875em;
}
h3, #toctitle, .sidebarblock > .content > .title {
font-size: 1.375em;
}
h4 {
font-size: 1.125em;
}
h5 {
font-size: 1.125em;
}
h6 {
font-size: 1em;
}
hr {
border: solid #ddddd8;
border-width: 1px 0 0;
clear: both;
margin: 1.25em 0 1.1875em;
height: 0;
}
/* Helpful Typography Defaults */
em, i {
font-style: italic;
line-height: inherit;
}
strong, b {
font-weight: bold;
line-height: inherit;
}
small {
font-size: 60%;
line-height: inherit;
}
code {
font-family: "Droid Sans Mono", "DejaVu Sans Mono", "Monospace", monospace;
font-weight: normal;
color: rgba(0, 0, 0, 0.9);
}
/* Lists */
ul, ol, dl {
font-size: 1em;
line-height: 1.6;
margin-bottom: 1.25em;
list-style-position: outside;
font-family: inherit;
}
ul, ol {
margin-left: 1.5em;
}
ul.no-bullet, ol.no-bullet {
margin-left: 1.5em;
}
/* Unordered Lists */
ul li ul, ul li ol {
margin-left: 1.25em;
margin-bottom: 0;
font-size: 1em; /* Override nested font-size change */
}
ul.square li ul, ul.circle li ul, ul.disc li ul {
list-style: inherit;
}
ul.square {
list-style-type: square;
}
ul.circle {
list-style-type: circle;
}
ul.disc {
list-style-type: disc;
}
ul.no-bullet {
list-style: none;
}
/* Ordered Lists */
ol li ul, ol li ol {
margin-left: 1.25em;
margin-bottom: 0;
}
/* Definition Lists */
dl dt {
margin-bottom: 0.3125em;
font-weight: bold;
}
dl dd {
margin-bottom: 1.25em;
}
/* Abbreviations */
abbr, acronym {
text-transform: uppercase;
font-size: 90%;
color: rgba(0, 0, 0, 0.8);
border-bottom: 1px dotted #dddddd;
cursor: help;
}
abbr {
text-transform: none;
}
/* Blockquotes */
blockquote {
margin: 0 0 1.25em;
padding: 0.5625em 1.25em 0 1.1875em;
border-left: 1px solid #dddddd;
}
blockquote cite {
display: block;
font-size: 0.9375em;
color: rgba(0, 0, 0, 0.6);
}
blockquote cite:before {
content: "\2014 \0020";
}
blockquote cite a, blockquote cite a:visited {
color: rgba(0, 0, 0, 0.6);
}
blockquote, blockquote p {
line-height: 1.6;
color: rgba(0, 0, 0, 0.85);
}
/* Microformats */
.vcard {
display: inline-block;
margin: 0 0 1.25em 0;
border: 1px solid #dddddd;
padding: 0.625em 0.75em;
}
.vcard li {
margin: 0;
display: block;
}
.vcard .fn {
font-weight: bold;
font-size: 0.9375em;
}
.vevent .summary {
font-weight: bold;
}
.vevent abbr {
cursor: auto;
text-decoration: none;
font-weight: bold;
border: none;
padding: 0 0.0625em;
}
@media only screen and (min-width: 768px) {
h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 {
line-height: 1.2;
}
h1 {
font-size: 2.75em;
}
h2 {
font-size: 2.3125em;
}
h3, #toctitle, .sidebarblock > .content > .title {
font-size: 1.6875em;
}
h4 {
font-size: 1.4375em;
}
}
/* Tables */
table {
background: white;
margin-bottom: 1.25em;
border: solid 1px #dedede;
}
table thead, table tfoot {
background: #f7f8f7;
font-weight: bold;
}
table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td {
padding: 0.5em 0.625em 0.625em;
font-size: inherit;
color: rgba(0, 0, 0, 0.8);
text-align: left;
}
table tr th, table tr td {
padding: 0.5625em 0.625em;
font-size: inherit;
color: rgba(0, 0, 0, 0.8);
}
table tr.even, table tr.alt, table tr:nth-of-type(even) {
background: #f8f8f7;
}
table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td {
display: table-cell;
line-height: 1.6;
}
h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 {
line-height: 1.2;
word-spacing: -0.05em;
}
h1 strong, h2 strong, h3 strong, #toctitle strong, .sidebarblock > .content > .title strong, h4 strong, h5 strong, h6 strong {
font-weight: 400;
}
.clearfix:before, .clearfix:after, .float-group:before, .float-group:after {
content: " ";
display: table;
}
.clearfix:after, .float-group:after {
clear: both;
}
*:not(pre) > code {
font-size: 0.9375em;
font-style: normal !important;
letter-spacing: 0;
padding: 0.1em 0.5ex;
word-spacing: -0.15em;
background-color: #f7f7f8;
-webkit-border-radius: 4px;
border-radius: 4px;
line-height: 1.45;
text-rendering: optimizeSpeed;
}
pre, pre > code {
line-height: 1.45;
color: rgba(0, 0, 0, 0.9);
font-family: "Droid Sans Mono", "DejaVu Sans Mono", "Monospace", monospace;
font-weight: normal;
text-rendering: optimizeSpeed;
}
.keyseq {
color: rgba(51, 51, 51, 0.8);
}
kbd {
display: inline-block;
color: rgba(0, 0, 0, 0.8);
font-size: 0.75em;
line-height: 1.4;
background-color: #f7f7f7;
border: 1px solid #ccc;
-webkit-border-radius: 3px;
border-radius: 3px;
-webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset;
margin: -0.15em 0.15em 0 0.15em;
padding: 0.2em 0.6em 0.2em 0.5em;
vertical-align: middle;
white-space: nowrap;
}
.keyseq kbd:first-child {
margin-left: 0;
}
.keyseq kbd:last-child {
margin-right: 0;
}
.menuseq, .menu {
color: rgba(0, 0, 0, 0.8);
}
b.button:before, b.button:after {
position: relative;
top: -1px;
font-weight: normal;
}
b.button:before {
content: "[";
padding: 0 3px 0 2px;
}
b.button:after {
content: "]";
padding: 0 2px 0 3px;
}
p a > code:hover {
color: rgba(0, 0, 0, 0.9);
}
#header, #content, #footnotes, #footer {
width: 100%;
margin-left: auto;
margin-right: auto;
margin-top: 0;
margin-bottom: 0;
max-width: 62.5em;
*zoom: 1;
position: relative;
padding-left: 0.9375em;
padding-right: 0.9375em;
}
#header:before, #header:after, #content:before, #content:after, #footnotes:before, #footnotes:after, #footer:before, #footer:after {
content: " ";
display: table;
}
#header:after, #content:after, #footnotes:after, #footer:after {
clear: both;
}
#content {
margin-top: 1.25em;
}
#content:before {
content: none;
}
#header > h1:first-child {
color: rgba(0, 0, 0, 0.85);
margin-top: 2.25rem;
margin-bottom: 0;
}
#header > h1:first-child + #toc {
margin-top: 8px;
border-top: 1px solid #ddddd8;
}
#header > h1:only-child, body.toc2 #header > h1:nth-last-child(2) {
border-bottom: 1px solid #ddddd8;
padding-bottom: 8px;
}
#header .details {
border-bottom: 1px solid #ddddd8;
line-height: 1.45;
padding-top: 0.25em;
padding-bottom: 0.25em;
padding-left: 0.25em;
color: rgba(0, 0, 0, 0.6);
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-ms-flex-flow: row wrap;
-webkit-flex-flow: row wrap;
flex-flow: row wrap;
}
#header .details span:first-child {
margin-left: -0.125em;
}
#header .details span.email a {
color: rgba(0, 0, 0, 0.85);
}
#header .details br {
display: none;
}
#header .details br + span:before {
content: "\00a0\2013\00a0";
}
#header .details br + span.author:before {
content: "\00a0\22c5\00a0";
color: rgba(0, 0, 0, 0.85);
}
#header .details br + span#revremark:before {
content: "\00a0|\00a0";
}
#header #revnumber {
text-transform: capitalize;
}
#header #revnumber:after {
content: "\00a0";
}
#content > h1:first-child:not([class]) {
color: rgba(0, 0, 0, 0.85);
border-bottom: 1px solid #ddddd8;
padding-bottom: 8px;
margin-top: 0;
padding-top: 1rem;
margin-bottom: 1.25rem;
}
#toc {
border-bottom: 1px solid #efefed;
padding-bottom: 0.5em;
}
#toc > ul {
margin-left: 0.125em;
}
#toc ul.sectlevel0 > li > a {
font-style: italic;
}
#toc ul.sectlevel0 ul.sectlevel1 {
margin: 0.5em 0;
}
#toc ul {
font-family: "Open Sans", "DejaVu Sans", sans-serif;
list-style-type: none;
}
#toc a {
text-decoration: none;
}
#toc a:active {
text-decoration: underline;
}
#toctitle {
color: #7a2518;
font-size: 1.2em;
}
@media only screen and (min-width: 768px) {
#toctitle {
font-size: 1.375em;
}
body.toc2 {
padding-left: 15em;
padding-right: 0;
}
#toc.toc2 {
margin-top: 0 !important;
background-color: #f8f8f7;
position: fixed;
width: 15em;
left: 0;
top: 0;
border-right: 1px solid #efefed;
border-top-width: 0 !important;
border-bottom-width: 0 !important;
z-index: 1000;
padding: 1.25em 1em;
height: 100%;
overflow: auto;
}
#toc.toc2 #toctitle {
margin-top: 0;
font-size: 1.2em;
}
#toc.toc2 > ul {
font-size: 0.9em;
margin-bottom: 0;
}
#toc.toc2 ul ul {
margin-left: 0;
padding-left: 1em;
}
#toc.toc2 ul.sectlevel0 ul.sectlevel1 {
padding-left: 0;
margin-top: 0.5em;
margin-bottom: 0.5em;
}
body.toc2.toc-right {
padding-left: 0;
padding-right: 15em;
}
body.toc2.toc-right #toc.toc2 {
border-right-width: 0;
border-left: 1px solid #efefed;
left: auto;
right: 0;
}
}
@media only screen and (min-width: 1280px) {
body.toc2 {
padding-left: 20em;
padding-right: 0;
}
#toc.toc2 {
width: 20em;
}
#toc.toc2 #toctitle {
font-size: 1.375em;
}
#toc.toc2 > ul {
font-size: 0.95em;
}
#toc.toc2 ul ul {
padding-left: 1.25em;
}
body.toc2.toc-right {
padding-left: 0;
padding-right: 20em;
}
}
#content #toc {
border-style: solid;
border-width: 1px;
border-color: #e0e0dc;
margin-bottom: 1.25em;
padding: 1.25em;
background: #f8f8f7;
-webkit-border-radius: 4px;
border-radius: 4px;
}
#content #toc > :first-child {
margin-top: 0;
}
#content #toc > :last-child {
margin-bottom: 0;
}
#footer {
max-width: 100%;
background-color: rgba(0, 0, 0, 0.8);
padding: 1.25em;
}
#footer-text {
color: rgba(255, 255, 255, 0.8);
line-height: 1.44;
}
.sect1 {
padding-bottom: 0.625em;
}
@media only screen and (min-width: 768px) {
.sect1 {
padding-bottom: 1.25em;
}
}
.sect1 + .sect1 {
border-top: 1px solid #efefed;
}
#content h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, #toctitle > a.anchor, .sidebarblock > .content > .title > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor {
position: absolute;
z-index: 1001;
width: 1.5ex;
margin-left: -1.5ex;
display: block;
text-decoration: none !important;
visibility: hidden;
text-align: center;
font-weight: normal;
}
#content h1 > a.anchor:before, h2 > a.anchor:before, h3 > a.anchor:before, #toctitle > a.anchor:before, .sidebarblock > .content > .title > a.anchor:before, h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before {
content: "\00A7";
font-size: 0.85em;
display: block;
padding-top: 0.1em;
}
#content h1:hover > a.anchor, #content h1 > a.anchor:hover, h2:hover > a.anchor, h2 > a.anchor:hover, h3:hover > a.anchor, #toctitle:hover > a.anchor, .sidebarblock > .content > .title:hover > a.anchor, h3 > a.anchor:hover, #toctitle > a.anchor:hover, .sidebarblock > .content > .title > a.anchor:hover, h4:hover > a.anchor, h4 > a.anchor:hover, h5:hover > a.anchor, h5 > a.anchor:hover, h6:hover > a.anchor, h6 > a.anchor:hover {
visibility: visible;
}
#content h1 > a.link, h2 > a.link, h3 > a.link, #toctitle > a.link, .sidebarblock > .content > .title > a.link, h4 > a.link, h5 > a.link, h6 > a.link {
color: #ba3925;
text-decoration: none;
}
#content h1 > a.link:hover, h2 > a.link:hover, h3 > a.link:hover, #toctitle > a.link:hover, .sidebarblock > .content > .title > a.link:hover, h4 > a.link:hover, h5 > a.link:hover, h6 > a.link:hover {
color: #a53221;
}
.audioblock, .imageblock, .literalblock, .listingblock, .stemblock, .videoblock {
margin-bottom: 1.25em;
}
.admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title {
text-rendering: optimizeLegibility;
text-align: left;
font-family: "Noto Serif", "DejaVu Serif", serif;
font-size: 1rem;
font-style: italic;
}
table.tableblock > caption.title {
white-space: nowrap;
overflow: visible;
max-width: 0;
}
.paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p {
color: rgba(0, 0, 0, 0.85);
}
table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p {
font-size: inherit;
}
.admonitionblock > table {
border-collapse: separate;
border: 0;
background: none;
width: 100%;
}
.admonitionblock > table td.icon {
text-align: center;
width: 80px;
}
.admonitionblock > table td.icon img {
max-width: none;
}
.admonitionblock > table td.icon .title {
font-weight: bold;
font-family: "Open Sans", "DejaVu Sans", sans-serif;
text-transform: uppercase;
}
.admonitionblock > table td.content {
padding-left: 1.125em;
padding-right: 1.25em;
border-left: 1px solid #ddddd8;
color: rgba(0, 0, 0, 0.6);
}
.admonitionblock > table td.content > :last-child > :last-child {
margin-bottom: 0;
}
.exampleblock > .content {
border-style: solid;
border-width: 1px;
border-color: #e6e6e6;
margin-bottom: 1.25em;
padding: 1.25em;
background: white;
-webkit-border-radius: 4px;
border-radius: 4px;
}
.exampleblock > .content > :first-child {
margin-top: 0;
}
.exampleblock > .content > :last-child {
margin-bottom: 0;
}
.sidebarblock {
border-style: solid;
border-width: 1px;
border-color: #e0e0dc;
margin-bottom: 1.25em;
padding: 1.25em;
background: #f8f8f7;
-webkit-border-radius: 4px;
border-radius: 4px;
}
.sidebarblock > :first-child {
margin-top: 0;
}
.sidebarblock > :last-child {
margin-bottom: 0;
}
.sidebarblock > .content > .title {
color: #7a2518;
margin-top: 0;
text-align: center;
}
.exampleblock > .content > :last-child > :last-child, .exampleblock > .content .olist > ol > li:last-child > :last-child, .exampleblock > .content .ulist > ul > li:last-child > :last-child, .exampleblock > .content .qlist > ol > li:last-child > :last-child, .sidebarblock > .content > :last-child > :last-child, .sidebarblock > .content .olist > ol > li:last-child > :last-child, .sidebarblock > .content .ulist > ul > li:last-child > :last-child, .sidebarblock > .content .qlist > ol > li:last-child > :last-child {
margin-bottom: 0;
}
.literalblock pre, .listingblock pre:not(.highlight), .listingblock pre[class="highlight"], .listingblock pre[class^="highlight "], .listingblock pre.CodeRay, .listingblock pre.prettyprint {
background: #f7f7f8;
}
.sidebarblock .literalblock pre, .sidebarblock .listingblock pre:not(.highlight), .sidebarblock .listingblock pre[class="highlight"], .sidebarblock .listingblock pre[class^="highlight "], .sidebarblock .listingblock pre.CodeRay, .sidebarblock .listingblock pre.prettyprint {
background: #f2f1f1;
}
.literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] {
-webkit-border-radius: 4px;
border-radius: 4px;
word-wrap: break-word;
padding: 1em;
font-size: 0.8125em;
}
.literalblock pre.nowrap, .literalblock pre[class].nowrap, .listingblock pre.nowrap, .listingblock pre[class].nowrap {
overflow-x: auto;
white-space: pre;
word-wrap: normal;
}
@media only screen and (min-width: 768px) {
.literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] {
font-size: 0.90625em;
}
}
@media only screen and (min-width: 1280px) {
.literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] {
font-size: 1em;
}
}
.literalblock.output pre {
color: #f7f7f8;
background-color: rgba(0, 0, 0, 0.9);
}
.listingblock pre.highlightjs {
padding: 0;
}
.listingblock pre.highlightjs > code {
padding: 1em;
-webkit-border-radius: 4px;
border-radius: 4px;
}
.listingblock pre.prettyprint {
border-width: 0;
}
.listingblock > .content {
position: relative;
}
.listingblock code[data-lang]:before {
display: none;
content: attr(data-lang);
position: absolute;
font-size: 0.75em;
top: 0.425rem;
right: 0.5rem;
line-height: 1;
text-transform: uppercase;
color: #999;
}
.listingblock:hover code[data-lang]:before {
display: block;
}
.listingblock.terminal pre .command:before {
content: attr(data-prompt);
padding-right: 0.5em;
color: #999;
}
.listingblock.terminal pre .command:not([data-prompt]):before {
content: "$";
}
table.pyhltable {
border-collapse: separate;
border: 0;
margin-bottom: 0;
background: none;
}
table.pyhltable td {
vertical-align: top;
padding-top: 0;
padding-bottom: 0;
}
table.pyhltable td.code {
padding-left: .75em;
padding-right: 0;
}
pre.pygments .lineno, table.pyhltable td:not(.code) {
color: #999;
padding-left: 0;
padding-right: .5em;
border-right: 1px solid #ddddd8;
}
pre.pygments .lineno {
display: inline-block;
margin-right: .25em;
}
table.pyhltable .linenodiv {
background: none !important;
padding-right: 0 !important;
}
.quoteblock {
margin: 0 1em 1.25em 1.5em;
display: table;
}
.quoteblock > .title {
margin-left: -1.5em;
margin-bottom: 0.75em;
}
.quoteblock blockquote, .quoteblock blockquote p {
color: rgba(0, 0, 0, 0.85);
font-size: 1.15rem;
line-height: 1.75;
word-spacing: 0.1em;
letter-spacing: 0;
font-style: italic;
text-align: justify;
}
.quoteblock blockquote {
margin: 0;
padding: 0;
border: 0;
}
.quoteblock blockquote:before {
content: "\201c";
float: left;
font-size: 2.75em;
font-weight: bold;
line-height: 0.6em;
margin-left: -0.6em;
color: #7a2518;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.quoteblock blockquote > .paragraph:last-child p {
margin-bottom: 0;
}
.quoteblock .attribution {
margin-top: 0.5em;
margin-right: 0.5ex;
text-align: right;
}
.quoteblock .quoteblock {
margin-left: 0;
margin-right: 0;
padding: 0.5em 0;
border-left: 3px solid rgba(0, 0, 0, 0.6);
}
.quoteblock .quoteblock blockquote {
padding: 0 0 0 0.75em;
}
.quoteblock .quoteblock blockquote:before {
display: none;
}
.verseblock {
margin: 0 1em 1.25em 1em;
}
.verseblock pre {
font-family: "Open Sans", "DejaVu Sans", sans;
font-size: 1.15rem;
color: rgba(0, 0, 0, 0.85);
font-weight: 300;
text-rendering: optimizeLegibility;
}
.verseblock pre strong {
font-weight: 400;
}
.verseblock .attribution {
margin-top: 1.25rem;
margin-left: 0.5ex;
}
.quoteblock .attribution, .verseblock .attribution {
font-size: 0.9375em;
line-height: 1.45;
font-style: italic;
}
.quoteblock .attribution br, .verseblock .attribution br {
display: none;
}
.quoteblock .attribution cite, .verseblock .attribution cite {
display: block;
letter-spacing: -0.05em;
color: rgba(0, 0, 0, 0.6);
}
.quoteblock.abstract {
margin: 0 0 1.25em 0;
display: block;
}
.quoteblock.abstract blockquote, .quoteblock.abstract blockquote p {
text-align: left;
word-spacing: 0;
}
.quoteblock.abstract blockquote:before, .quoteblock.abstract blockquote p:first-of-type:before {
display: none;
}
table.tableblock {
max-width: 100%;
border-collapse: separate;
}
table.tableblock td > .paragraph:last-child p > p:last-child, table.tableblock th > p:last-child, table.tableblock td > p:last-child {
margin-bottom: 0;
}
table.spread {
width: 100%;
}
table.tableblock, th.tableblock, td.tableblock {
border: 0 solid #dedede;
}
table.grid-all th.tableblock, table.grid-all td.tableblock {
border-width: 0 1px 1px 0;
}
table.grid-all tfoot > tr > th.tableblock, table.grid-all tfoot > tr > td.tableblock {
border-width: 1px 1px 0 0;
}
table.grid-cols th.tableblock, table.grid-cols td.tableblock {
border-width: 0 1px 0 0;
}
table.grid-all * > tr > .tableblock:last-child, table.grid-cols * > tr > .tableblock:last-child {
border-right-width: 0;
}
table.grid-rows th.tableblock, table.grid-rows td.tableblock {
border-width: 0 0 1px 0;
}
table.grid-all tbody > tr:last-child > th.tableblock, table.grid-all tbody > tr:last-child > td.tableblock, table.grid-all thead:last-child > tr > th.tableblock, table.grid-rows tbody > tr:last-child > th.tableblock, table.grid-rows tbody > tr:last-child > td.tableblock, table.grid-rows thead:last-child > tr > th.tableblock {
border-bottom-width: 0;
}
table.grid-rows tfoot > tr > th.tableblock, table.grid-rows tfoot > tr > td.tableblock {
border-width: 1px 0 0 0;
}
table.frame-all {
border-width: 1px;
}
table.frame-sides {
border-width: 0 1px;
}
table.frame-topbot {
border-width: 1px 0;
}
th.halign-left, td.halign-left {
text-align: left;
}
th.halign-right, td.halign-right {
text-align: right;
}
th.halign-center, td.halign-center {
text-align: center;
}
th.valign-top, td.valign-top {
vertical-align: top;
}
th.valign-bottom, td.valign-bottom {
vertical-align: bottom;
}
th.valign-middle, td.valign-middle {
vertical-align: middle;
}
table thead th, table tfoot th {
font-weight: bold;
}
tbody tr th {
display: table-cell;
line-height: 1.6;
background: #f7f8f7;
}
tbody tr th, tbody tr th p, tfoot tr th, tfoot tr th p {
color: rgba(0, 0, 0, 0.8);
font-weight: bold;
}
p.tableblock > code:only-child {
background: none;
padding: 0;
}
p.tableblock {
font-size: 1em;
}
td > div.verse {
white-space: pre;
}
ol {
margin-left: 1.75em;
}
ul li ol {
margin-left: 1.5em;
}
dl dd {
margin-left: 1.125em;
}
dl dd:last-child, dl dd:last-child > :last-child {
margin-bottom: 0;
}
ol > li p, ul > li p, ul dd, ol dd, .olist .olist, .ulist .ulist, .ulist .olist, .olist .ulist {
margin-bottom: 0.625em;
}
ul.unstyled, ol.unnumbered, ul.checklist, ul.none {
list-style-type: none;
}
ul.unstyled, ol.unnumbered, ul.checklist {
margin-left: 0.625em;
}
ul.checklist li > p:first-child > .fa-check-square-o:first-child, ul.checklist li > p:first-child > input[type="checkbox"]:first-child {
margin-right: 0.25em;
}
ul.checklist li > p:first-child > input[type="checkbox"]:first-child {
position: relative;
top: 1px;
}
ul.inline {
margin: 0 auto 0.625em auto;
margin-left: -1.375em;
margin-right: 0;
padding: 0;
list-style: none;
overflow: hidden;
}
ul.inline > li {
list-style: none;
float: left;
margin-left: 1.375em;
display: block;
}
ul.inline > li > * {
display: block;
}
.unstyled dl dt {
font-weight: normal;
font-style: normal;
}
ol.arabic {
list-style-type: decimal;
}
ol.decimal {
list-style-type: decimal-leading-zero;
}
ol.loweralpha {
list-style-type: lower-alpha;
}
ol.upperalpha {
list-style-type: upper-alpha;
}
ol.lowerroman {
list-style-type: lower-roman;
}
ol.upperroman {
list-style-type: upper-roman;
}
ol.lowergreek {
list-style-type: lower-greek;
}
.hdlist > table, .colist > table {
border: 0;
background: none;
}
.hdlist > table > tbody > tr, .colist > table > tbody > tr {
background: none;
}
td.hdlist1 {
padding-right: .75em;
font-weight: bold;
}
td.hdlist1, td.hdlist2 {
vertical-align: top;
}
.literalblock + .colist, .listingblock + .colist {
margin-top: -0.5em;
}
.colist > table tr > td:first-of-type {
padding: 0 0.75em;
line-height: 1;
}
.colist > table tr > td:last-of-type {
padding: 0.25em 0;
}
.thumb, .th {
line-height: 0;
display: inline-block;
border: solid 4px white;
-webkit-box-shadow: 0 0 0 1px #dddddd;
box-shadow: 0 0 0 1px #dddddd;
}
.imageblock.left, .imageblock[style*="float: left"] {
margin: 0.25em 0.625em 1.25em 0;
}
.imageblock.right, .imageblock[style*="float: right"] {
margin: 0.25em 0 1.25em 0.625em;
}
.imageblock > .title {
margin-bottom: 0;
}
.imageblock.thumb, .imageblock.th {
border-width: 6px;
}
.imageblock.thumb > .title, .imageblock.th > .title {
padding: 0 0.125em;
}
.image.left, .image.right {
margin-top: 0.25em;
margin-bottom: 0.25em;
display: inline-block;
line-height: 0;
}
.image.left {
margin-right: 0.625em;
}
.image.right {
margin-left: 0.625em;
}
a.image {
text-decoration: none;
}
span.footnote, span.footnoteref {
vertical-align: super;
font-size: 0.875em;
}
span.footnote a, span.footnoteref a {
text-decoration: none;
}
span.footnote a:active, span.footnoteref a:active {
text-decoration: underline;
}
#footnotes {
padding-top: 0.75em;
padding-bottom: 0.75em;
margin-bottom: 0.625em;
}
#footnotes hr {
width: 20%;
min-width: 6.25em;
margin: -.25em 0 .75em 0;
border-width: 1px 0 0 0;
}
#footnotes .footnote {
padding: 0 0.375em;
line-height: 1.3;
font-size: 0.875em;
margin-left: 1.2em;
text-indent: -1.2em;
margin-bottom: .2em;
}
#footnotes .footnote a:first-of-type {
font-weight: bold;
text-decoration: none;
}
#footnotes .footnote:last-of-type {
margin-bottom: 0;
}
#content #footnotes {
margin-top: -0.625em;
margin-bottom: 0;
padding: 0.75em 0;
}
.gist .file-data > table {
border: 0;
background: #fff;
width: 100%;
margin-bottom: 0;
}
.gist .file-data > table td.line-data {
width: 99%;
}
div.unbreakable {
page-break-inside: avoid;
}
.big {
font-size: larger;
}
.small {
font-size: smaller;
}
.underline {
text-decoration: underline;
}
.overline {
text-decoration: overline;
}
.line-through {
text-decoration: line-through;
}
.aqua {
color: #00bfbf;
}
.aqua-background {
background-color: #00fafa;
}
.black {
color: black;
}
.black-background {
background-color: black;
}
.blue {
color: #0000bf;
}
.blue-background {
background-color: #0000fa;
}
.fuchsia {
color: #bf00bf;
}
.fuchsia-background {
background-color: #fa00fa;
}
.gray {
color: #606060;
}
.gray-background {
background-color: #7d7d7d;
}
.green {
color: #006000;
}
.green-background {
background-color: #007d00;
}
.lime {
color: #00bf00;
}
.lime-background {
background-color: #00fa00;
}
.maroon {
color: #600000;
}
.maroon-background {
background-color: #7d0000;
}
.navy {
color: #000060;
}
.navy-background {
background-color: #00007d;
}
.olive {
color: #606000;
}
.olive-background {
background-color: #7d7d00;
}
.purple {
color: #600060;
}
.purple-background {
background-color: #7d007d;
}
.red {
color: #bf0000;
}
.red-background {
background-color: #fa0000;
}
.silver {
color: #909090;
}
.silver-background {
background-color: #bcbcbc;
}
.teal {
color: #006060;
}
.teal-background {
background-color: #007d7d;
}
.white {
color: #bfbfbf;
}
.white-background {
background-color: #fafafa;
}
.yellow {
color: #bfbf00;
}
.yellow-background {
background-color: #fafa00;
}
span.icon > .fa {
cursor: default;
}
.admonitionblock td.icon [class^="fa icon-"] {
font-size: 2.5em;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
cursor: default;
}
.admonitionblock td.icon .icon-note:before {
content: "\f05a";
color: #19407c;
}
.admonitionblock td.icon .icon-tip:before {
content: "\f0eb";
text-shadow: 1px 1px 2px rgba(155, 155, 0, 0.8);
color: #111;
}
.admonitionblock td.icon .icon-warning:before {
content: "\f071";
color: #bf6900;
}
.admonitionblock td.icon .icon-caution:before {
content: "\f06d";
color: #bf3400;
}
.admonitionblock td.icon .icon-important:before {
content: "\f06a";
color: #bf0000;
}
.conum[data-value] {
display: inline-block;
color: #fff !important;
background-color: rgba(0, 0, 0, 0.8);
-webkit-border-radius: 100px;
border-radius: 100px;
text-align: center;
font-size: 0.75em;
width: 1.67em;
height: 1.67em;
line-height: 1.67em;
font-family: "Open Sans", "DejaVu Sans", sans-serif;
font-style: normal;
font-weight: bold;
}
.conum[data-value] * {
color: #fff !important;
}
.conum[data-value] + b {
display: none;
}
.conum[data-value]:after {
content: attr(data-value);
}
pre .conum[data-value] {
position: relative;
top: -0.125em;
}
b.conum * {
color: inherit !important;
}
.conum:not([data-value]):empty {
display: none;
}
h1, h2 {
letter-spacing: -0.01em;
}
dt, th.tableblock, td.content {
text-rendering: optimizeLegibility;
}
p, td.content {
letter-spacing: -0.01em;
}
p strong, td.content strong {
letter-spacing: -0.005em;
}
p, blockquote, dt, td.content {
font-size: 1.0625rem;
}
p {
margin-bottom: 1.25rem;
}
.sidebarblock p, .sidebarblock dt, .sidebarblock td.content, p.tableblock {
font-size: 1em;
}
.exampleblock > .content {
background-color: #fffef7;
border-color: #e0e0dc;
-webkit-box-shadow: 0 1px 4px #e0e0dc;
box-shadow: 0 1px 4px #e0e0dc;
}
.print-only {
display: none !important;
}
@media print {
@page {
margin: 1.25cm 0.75cm;
}
* {
-webkit-box-shadow: none !important;
box-shadow: none !important;
text-shadow: none !important;
}
a {
color: inherit !important;
text-decoration: underline !important;
}
a.bare, a[href^="#"], a[href^="mailto:"] {
text-decoration: none !important;
}
a[href^="http:"]:not(.bare):after, a[href^="https:"]:not(.bare):after, a[href^="mailto:"]:not(.bare):after {
content: "(" attr(href) ")";
display: inline-block;
font-size: 0.875em;
padding-left: 0.25em;
}
abbr[title]:after {
content: " (" attr(title) ")";
}
pre, blockquote, tr, img {
page-break-inside: avoid;
}
thead {
display: table-header-group;
}
img {
max-width: 100% !important;
}
p, blockquote, dt, td.content {
font-size: 1em;
orphans: 3;
widows: 3;
}
h2, h3, #toctitle, .sidebarblock > .content > .title, #toctitle, .sidebarblock > .content > .title {
page-break-after: avoid;
}
#toc, .sidebarblock, .exampleblock > .content {
background: none !important;
}
#toc {
border-bottom: 1px solid #ddddd8 !important;
padding-bottom: 0 !important;
}
.sect1 {
padding-bottom: 0 !important;
}
.sect1 + .sect1 {
border: 0 !important;
}
#header > h1:first-child {
margin-top: 1.25rem;
}
body.book #header {
text-align: center;
}
body.book #header > h1:first-child {
border: 0 !important;
margin: 2.5em 0 1em 0;
}
body.book #header .details {
border: 0 !important;
display: block;
padding: 0 !important;
}
body.book #header .details span:first-child {
margin-left: 0 !important;
}
body.book #header .details br {
display: block;
}
body.book #header .details br + span:before {
content: none !important;
}
body.book #toc {
border: 0 !important;
text-align: left !important;
padding: 0 !important;
margin: 0 !important;
}
body.book #toc, body.book #preamble, body.book h1.sect0, body.book .sect1 > h2 {
page-break-before: always;
}
.listingblock code[data-lang]:before {
display: block;
}
#footer {
background: none !important;
padding: 0 0.9375em;
}
#footer-text {
color: rgba(0, 0, 0, 0.6) !important;
font-size: 0.9em;
}
.hide-on-print {
display: none !important;
}
.print-only {
display: block !important;
}
.hide-for-print {
display: none !important;
}
.show-for-print {
display: inherit !important;
}
}
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.4.0/css/font-awesome.min.css">
</head>
<body class="article">
<div id="header">
<h1>Testing Ratpack Applications</h1>
<div class="details">
<span id="author" class="author">Dan Hyun</span><br>
<span id="email" class="email">@LSpacewalker</span><br>
</div>
</div>
<div id="content">
<div id="preamble">
<div class="sectionbody">
<div class="paragraph">
<p>Ratpack is a developer friendly and productivity focused web framework.
That&#8217;s quite a claim to make.
We&#8217;ll explore how Ratpack&#8217;s rich testing facilities strongly support this statement.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="trueintro"><a class="anchor" href="#trueintro"></a>Intro</h2>
<div class="sectionbody">
<div class="ulist">
<ul>
<li>
<p>Test framework Agnostic (Spock, JUnit, TestNG)</p>
</li>
<li>
<p>Core fixutres in Java 8+, first-class Groovy Support available</p>
</li>
<li>
<p>Most fixtures implement <code>java.lang.AutoCloseable</code></p>
<div class="ulist">
<ul>
<li>
<p>Need to either close yourself or use in <code>try-with-resources</code></p>
</li>
<li>
<p>Provides points of interaction that utilize an execute around pattern in cases where you need the fixture once.</p>
</li>
</ul>
</div>
</li>
</ul>
</div>
</div>
</div>
<div class="sect1">
<h2 id="truehello-world"><a class="anchor" href="#truehello-world"></a>Hello World</h2>
<div class="sectionbody">
<div class="sect2">
<h3 id="truedependencies"><a class="anchor" href="#truedependencies"></a>Dependencies</h3>
<div class="listingblock">
<div class="title">testing-ratpack-apps.gradle</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-gradle" data-lang="gradle">plugins { <i class="conum" data-value="1"></i><b>(1)</b>
id 'io.ratpack.ratpack-groovy' version '1.2.0' <i class="conum" data-value="2"></i><b>(2)</b>
}
repositories {
jcenter()
}
dependencies {
runtime "org.apache.logging.log4j:log4j-slf4j-impl:${log4j}"
runtime "org.apache.logging.log4j:log4j-api:${log4j}"
runtime "org.apache.logging.log4j:log4j-core:${log4j}"
runtime 'com.lmax:disruptor:3.3.2'
testCompile ratpack.dependency('groovy-test') <i class="conum" data-value="3"></i><b>(3)</b>
testCompile ('org.spockframework:spock-core:1.0-groovy-2.4') {
exclude module: "groovy-all"
}
testCompile 'junit:junit:4.12'
testCompile 'org.testng:testng:6.9.10'
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Use Gradle&#8217;s incubating Plugins feature</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Pull in and apply Ratpack&#8217;s Gradle plugin from Gradle&#8217;s Plugin Portal</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>Pull in <code>'io.ratpack:ratpack-groovy-test</code> from Bintray</td>
</tr>
</table>
</div>
</div>
<div class="sect2">
<h3 id="truehello-world-under-test"><a class="anchor" href="#truehello-world-under-test"></a>Hello World Under Test</h3>
<div class="listingblock">
<div class="title">ratpack.groovy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import static ratpack.groovy.Groovy.ratpack
ratpack {
handlers {
get {
render 'Hello Greach 2016!'
}
}
}</code></pre>
</div>
</div>
<div class="listingblock">
<div class="title">MainClassApp</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import groovy.transform.CompileStatic
import ratpack.func.Action
import ratpack.groovy.handling.GroovyContext
import ratpack.groovy.handling.GroovyHandler
import ratpack.handling.Chain
import ratpack.server.RatpackServer
import ratpack.server.RatpackServerSpec
@CompileStatic
class MainClassApp {
public static void main(String[] args) throws Exception {
RatpackServer.start({ RatpackServerSpec serverSpec -&gt; serverSpec
.handlers({ Chain chain -&gt;
chain.get({GroovyContext ctx -&gt;
ctx.render 'Hello Greach 2016!'
} as GroovyHandler)
} as Action&lt;Chain&gt;)
} as Action&lt;RatpackServerSpec&gt;)
}
}</code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="trueverify"><a class="anchor" href="#trueverify"></a>Verify</h3>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-bash" data-lang="bash">$ curl localhost:5050
Hello Greach 2016!</code></pre>
</div>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="true-code-assert-true-code"><a class="anchor" href="#true-code-assert-true-code"></a><code>assert true</code></h2>
<div class="sectionbody">
<div class="sect2">
<h3 id="truespock-hello-world"><a class="anchor" href="#truespock-hello-world"></a>Spock Hello World</h3>
<div class="listingblock">
<div class="title">HelloWorldSpec.groovy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import ratpack.groovy.test.GroovyRatpackMainApplicationUnderTest
import ratpack.test.MainClassApplicationUnderTest
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll
class HelloWorldSpec extends Specification {
// tag::GroovyScriptAUT[]
@AutoCleanup
@Shared
GroovyRatpackMainApplicationUnderTest groovyScriptApplicationunderTest = new GroovyRatpackMainApplicationUnderTest()
// end::GroovyScriptAUT[]
// tag::MainClassAUT[]
@AutoCleanup
@Shared
MainClassApplicationUnderTest mainClassApplicationUnderTest = new MainClassApplicationUnderTest(MainClassApp)
// end::MainClassAUT[]
@Unroll
def 'Should render \'Hello Greach 2016!\' from #type'() {
when:
def getText = aut.httpClient.getText()
then:
getText == 'Hello Greach 2016!'
where:
aut | type
groovyScriptApplicationunderTest | 'ratpack.groovy'
mainClassApplicationUnderTest | 'MainClassApp.groovy'
}
}</code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="truejunit-test"><a class="anchor" href="#truejunit-test"></a>Junit Test</h3>
<div class="listingblock">
<div class="title">HelloJunitTest.groovy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import groovy.transform.CompileStatic
import org.junit.Assert
import org.junit.AfterClass
import org.junit.BeforeClass
import org.junit.Test
import ratpack.groovy.test.GroovyRatpackMainApplicationUnderTest
import ratpack.test.MainClassApplicationUnderTest
import ratpack.test.http.TestHttpClient
import static org.junit.Assert.assertEquals
@CompileStatic
class HelloJunitTest {
static GroovyRatpackMainApplicationUnderTest groovyScriptApplicationunderTest
static MainClassApplicationUnderTest mainClassApplicationUnderTest
@BeforeClass
static void setup() {
groovyScriptApplicationunderTest = new GroovyRatpackMainApplicationUnderTest()
mainClassApplicationUnderTest = new MainClassApplicationUnderTest(MainClassApp)
}
@AfterClass
static void cleanup() {
groovyScriptApplicationunderTest.close()
mainClassApplicationUnderTest.close()
}
@Test
def void testHelloWorld() {
[
groovyScriptApplicationunderTest,
mainClassApplicationUnderTest
].each { aut -&gt;
TestHttpClient client = aut.httpClient
assertEquals('Hello Greach 2016!', client.getText())
}
}
}</code></pre>
</div>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="trueunit-testing"><a class="anchor" href="#trueunit-testing"></a>Unit testing</h2>
<div class="sectionbody">
<div class="sect2">
<h3 id="true-code-groovyrequestfixture-code"><a class="anchor" href="#true-code-groovyrequestfixture-code"></a><code>GroovyRequestFixture</code></h3>
<div class="sect3">
<h4 id="truetesting-standalone-handlers"><a class="anchor" href="#truetesting-standalone-handlers"></a>Testing Standalone Handlers</h4>
<div class="listingblock">
<div class="title">ImportantHandler.groovy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import groovy.transform.CompileStatic
import ratpack.groovy.handling.GroovyContext
import ratpack.groovy.handling.GroovyHandler
@CompileStatic
class ImportantHandler extends GroovyHandler {
@Override
protected void handle(GroovyContext context) {
context.render 'Very important handler'
}
}</code></pre>
</div>
</div>
<div class="listingblock">
<div class="title">ratpack.groovy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import static ratpack.groovy.Groovy.ratpack
ratpack {
handlers {
get(new ImportantHandler()) <i class="conum" data-value="1"></i><b>(1)</b>
}
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Bind our <code>ImportantHandler</code> to <code>GET /</code></td>
</tr>
</table>
</div>
<div class="listingblock">
<div class="title">Failing test</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">def 'should render \'Very important handler\''() {
when:
HandlingResult result = GroovyRequestFixture.handle(new ImportantHandler()) {}
then:
result.bodyText == 'Very important handler <i class="conum" data-value="1"></i><b>(1)</b>
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Consult the <code>HandlingResult</code> for response body</td>
</tr>
</table>
</div>
<div class="paragraph">
<p>WARN: This test will fail</p>
</div>
<div class="paragraph">
<p>What happened?</p>
</div>
<div class="paragraph">
<p><code>Context#render(Object)</code> uses Ratpack&#8217;s rendering framework.
<code>GroovyRequestFixture</code> does not actually serialize rendered objects to <code>Response</code> of <code>HandlingResult</code>.
For this test to pass you must either modify the Handler or modify the expectation:</p>
</div>
<div class="paragraph">
<p>Modify the handler:</p>
</div>
<div class="listingblock">
<div class="title">ImportantSendingHandler.groovy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import groovy.transform.CompileStatic
import ratpack.groovy.handling.GroovyContext
import ratpack.groovy.handling.GroovyHandler
@CompileStatic
class ImportantSendingHandler extends GroovyHandler {
@Override
protected void handle(GroovyContext context) {
context.response.send('Very important handler')
}
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>Modify the expectation:</p>
</div>
<div class="listingblock">
<div class="title">ImportantHandlerUnitSpec.groovy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy"> def 'should render \'Very important handler\''() {
when:
HandlingResult result = GroovyRequestFixture.handle(new ImportantHandler()) {}
then:
String rendered = result.rendered(String) <i class="conum" data-value="1"></i><b>(1)</b>
rendered == 'Very important handler'
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Retrieve the rendered object by type from <code>HandlingResult</code></td>
</tr>
</table>
</div>
<div class="paragraph">
<p>Everday use:</p>
</div>
</div>
<div class="sect3">
<h4 id="truemodify-request-attributes"><a class="anchor" href="#truemodify-request-attributes"></a>Modify request attributes</h4>
<div class="listingblock">
<div class="title">HeaderExtractionHandler.groovy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import groovy.transform.CompileStatic
import ratpack.groovy.handling.GroovyContext
import ratpack.groovy.handling.GroovyHandler
@CompileStatic
class HeaderExtractionHandler extends GroovyHandler {
@Override
protected void handle(GroovyContext context) {
String specialHeader = context.request.headers.get('special') ?: 'not special' <i class="conum" data-value="1"></i><b>(1)</b>
context.render "Special header: $specialHeader"
}
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Extract HTTP header and render a response to client</td>
</tr>
</table>
</div>
<div class="listingblock">
<div class="title">HeaderExtractionHandlingSpec.groovy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import ratpack.test.handling.HandlingResult
import spock.lang.Specification
import ratpack.groovy.test.handling.GroovyRequestFixture
import spock.lang.Unroll
class HeaderExtractionHandlingSpec extends Specification {
@Unroll
def 'should render #expectedValue with special header value'() {
when:
HandlingResult result = GroovyRequestFixture
.handle(new HeaderExtractionHandler(), requestFixture)
then:
def rendered = result.rendered(CharSequence)
rendered == "Special header: $expectedValue"
where:
expectedValue | requestFixture
'greach2016' | { header('special', 'greach2016') } <i class="conum" data-value="1"></i><b>(1)</b>
'not special' | {}
}
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>You can get a chance to configure the properties of the request to be made, can configure HTTP method, headers, request body, etc.</td>
</tr>
</table>
</div>
</div>
<div class="sect3">
<h4 id="truemodify-and-make-assertions-against-context-registry"><a class="anchor" href="#truemodify-and-make-assertions-against-context-registry"></a>Modify and make assertions against context registry:</h4>
<div class="listingblock">
<div class="title">ProfileLoadingHandler.groovy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import groovy.transform.Canonical
import groovy.transform.CompileStatic
import ratpack.groovy.handling.GroovyContext
import ratpack.groovy.handling.GroovyHandler
import ratpack.registry.Registry
@CompileStatic
class ProfileLoadingHandler extends GroovyHandler {
@Override
protected void handle(GroovyContext context) {
String role = context.request.headers.get('role') ?: 'guest' <i class="conum" data-value="1"></i><b>(1)</b>
String secretToken = context.get(String) <i class="conum" data-value="2"></i><b>(2)</b>
context.next(Registry.single(new Profile(role: role, token: secretToken))) <i class="conum" data-value="3"></i><b>(3)</b>
}
}
@Canonical
class Profile {
String role
String token
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Extract role from request header, defaulting to 'guest'</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Extract a String from the context registry</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>Delegate to the next Handler in the chain and pass a new single Registry with a newly constructed Profile object</td>
</tr>
</table>
</div>
<div class="paragraph">
<p>We can make use of <code>RequestFixture</code> to populate the Registry with any entries our stand-alone Handler may be expecting, such as a token in the form of a String.</p>
</div>
<div class="listingblock">
<div class="title">ProfileLoadingHandlingSpec.groovy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy"> def 'handler should populate context registry with Profile'() {
when:
HandlingResult result = GroovyRequestFixture.handle(new ProfileLoadingHandler()) {
header('role', 'admin') <i class="conum" data-value="1"></i><b>(1)</b>
registry { add(String, 'secret-token') } <i class="conum" data-value="2"></i><b>(2)</b>
}
then:
result.registry.get(Profile) == new Profile('admin', 'secret-token') <i class="conum" data-value="3"></i><b>(3)</b>
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Use <code>RequestFixture#header</code> to add Headers to the HTTP Request</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Use <code>RequestFixture#registry</code> to add a <code>String</code> to the Context registry</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>Consult the HandlingResponse to ensure that the context was populated with a <code>Profile</code> object and that it meets our expectations</td>
</tr>
</table>
</div>
<div class="paragraph">
<p>Let&#8217;s put our <code>ProfileLoadingHandler</code> in a chain with a dummy Map renderer:</p>
</div>
<div class="listingblock">
<div class="title">ProfileLoadingHandlingSpec.groovy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy"> def 'should be able to render Profile as map from Registry'() {
when:
HandlingResult result = GroovyRequestFixture.handle(new GroovyChainAction() { <i class="conum" data-value="1"></i><b>(1)</b>
@Override
void execute() throws Exception {
all(new ProfileLoadingHandler()) <i class="conum" data-value="2"></i><b>(2)</b>
get { <i class="conum" data-value="3"></i><b>(3)</b>
Profile profile = get(Profile)
render([profile: [role: profile.role, token: profile.token]])
}
}
}) {
header('role', 'admin')
registry { add(String, 'secret-token') }
}
then:
result.rendered(Map) == [profile: [role: 'admin', 'token': 'secret-token']] <i class="conum" data-value="4"></i><b>(4)</b>
}</code></pre>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="true-code-groovyembeddedapp-code"><a class="anchor" href="#true-code-groovyembeddedapp-code"></a><code>GroovyEmbeddedApp</code></h2>
<div class="sectionbody">
<div class="paragraph">
<p><code>GroovyEmbeddedApp</code> represents an isolated subset of functionality that stands up a full Ratpack server.</p>
</div>
<div class="paragraph">
<p>It represents a very bare server that binds to an ephemeral port and has no base directory by default.
<code>GroovyEmbeddedApp</code> is also <code>AutoCloseable</code>.
If you plan on making more than a few interactions it may help to grab a <code>TestHttpClient</code> from the server, otherwise you can make use of <code>EmbeddedApp#test(TestHttpClient)</code> which will ensure that the <code>EmbeddedApp</code> is shut down gracefully.
Javadocs for Ratpack are 100% tested and make use of <code>EmbeddedApp</code> to demonstrate functionality.</p>
</div>
<div class="paragraph">
<p>The <code>EmbeddedApp</code> is also useful in creating a test fixture that represents some network based resource that returns canned or contrived responses.</p>
</div>
<div class="listingblock">
<div class="title">EmbeddedAppSpec</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy"> def 'can create simple hello world'() {
expect:
GroovyEmbeddedApp.fromHandler { <i class="conum" data-value="1"></i><b>(1)</b>
render 'Hello Greach 2016!'
} test {
assert getText() == 'Hello Greach 2016!' <i class="conum" data-value="2"></i><b>(2)</b>
}
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Creates a full Ratpack server with a single handler</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Ratpack provides us with a <code>TestHttpClient</code> that is configured to submit requests to <code>EmbeddedApp</code>. When the closure is finished executing Ratpack will take care of cleaning up the <code>EmbeddedApp</code>.</td>
</tr>
</table>
</div>
</div>
</div>
<div class="sect1">
<h2 id="truetesthttpclient"><a class="anchor" href="#truetesthttpclient"></a>TestHttpClient</h2>
<div class="sectionbody">
<div class="paragraph">
<p>For testing, Ratpack provides <code>TestHttpClient</code> which is a blocking, synchronous http client for making requests against a running <code>ApplicationUnderTest</code>. This is intentionally designed in order to make testing deterministic and predictable.</p>
</div>
<div class="listingblock">
<div class="title">EmbeddedAppSpec</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy"> def 'demonstrate ByMethodSpec'() {
given:
GroovyEmbeddedApp app = GroovyEmbeddedApp.fromHandlers { <i class="conum" data-value="1"></i><b>(1)</b>
path {
byMethod {
get {
render 'GET'
}
post {
render 'POST'
}
}
}
}
and:
TestHttpClient client = app.httpClient <i class="conum" data-value="2"></i><b>(2)</b>
expect: <i class="conum" data-value="3"></i><b>(3)</b>
client.getText() == 'GET'
client.postText() == 'POST'
client.put().status.code == 405
client.delete().status.code == 405
cleanup: <i class="conum" data-value="4"></i><b>(4)</b>
app.close()
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Create <code>GroovyEmbeddedApp</code> from a chain</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Retrieve a configured <code>TestHttpClient</code> for making requests against the <code>EmbeddedApp</code></td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>Make some assertions about the application as described by the chain</td>
</tr>
<tr>
<td><i class="conum" data-value="4"></i><b>4</b></td>
<td>Have Spock invoke <code>EmbeddedApp#close</code> to gracefully shutdown the server.</td>
</tr>
</table>
</div>
<div class="paragraph">
<p>The <code>TestHttpClient</code> has some basic support for manipulating request configuration as well as handling redirects and cookies.</p>
</div>
<div class="listingblock">
<div class="title">EmbeddedAppSpec</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy"> def 'should handle redirects and cookies'() {
expect:
GroovyEmbeddedApp.fromHandlers { <i class="conum" data-value="1"></i><b>(1)</b>
get {
render request.oneCookie('foo') ?: 'empty'
}
get('set') {
response.cookie('foo', 'foo')
redirect '/'
}
get('clear') {
response.expireCookie('foo')
redirect '/'
}
} test {
assert getText() == 'empty' <i class="conum" data-value="2"></i><b>(2)</b>
assert getCookies('/')*.name() == []
assert getCookies('/')*.value() == []
assert getText('set') == 'foo'
assert getCookies('/')*.name() == ['foo']
assert getCookies('/')*.value() == ['foo']
assert getText() == 'foo'
assert getText('clear') == 'empty'
assert getCookies('/')*.name() == []
assert getCookies('/')*.value() == []
assert getText() == 'empty'
assert getCookies('/')*.name() == []
assert getCookies('/')*.value() == []
}
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Create sample app that reads and writes cookies</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Issue requests that ensures cookie setting/expiring and redirect functionality</td>
</tr>
</table>
</div>
</div>
</div>
<div class="sect1">
<h2 id="trueasync-testing"><a class="anchor" href="#trueasync-testing"></a>Async Testing</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Ratpack is asynchronous and non-blocking from the ground up. This means that not only is Ratpack&#8217;s api asynchronous but it expects that your code should be asynchronous as well.</p>
</div>
<div class="paragraph">
<p>Let&#8217;s say we have a <code>ProfileService</code> that&#8217;s responsible for retrieving `Profile`s:</p>
</div>
<div class="listingblock">
<div class="title">ProfileService.groovy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import groovy.transform.Canonical
import ratpack.exec.Operation
import ratpack.exec.Promise
class ProfileService {
final List&lt;Profile&gt; profiles = []
Promise&lt;List&lt;Profile&gt;&gt; getProfiles() {
Promise.value(profiles)
}
Operation add(Profile p) {
profiles.add(p)
Operation.noop()
}
Operation delete() {
profiles.clear()
Operation.noop()
}
}
@Canonical
class Profile {
String role
String token
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>If you were to test this Service without any assistance from Ratpack you will run into the well known <code>UnmanagedThreadException</code>:</p>
</div>
<div class="listingblock">
<div class="content">
<pre>ratpack.exec.UnmanagedThreadException: Operation attempted on non Ratpack managed thread</pre>
</div>
</div>
<div class="sect2">
<h3 id="trueexecharness"><a class="anchor" href="#trueexecharness"></a>ExecHarness</h3>
<div class="paragraph">
<p><code>ExecHarness</code> is the utility that Ratpack provides to test any kind of asynchronous behavior.
Unsurprisingly <code>ExecHarness</code> is also an <code>AutoCloseable</code>.
It utilizes resources that manage an <code>EventLoopGroup</code> and an <code>ExecutorService</code> so it&#8217;s important to make sure these resources get properly cleaned up.</p>
</div>
<div class="listingblock">
<div class="title">ProfileServiceExec.groovy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import ratpack.exec.ExecResult
import ratpack.exec.Promise
import ratpack.test.exec.ExecHarness
import spock.lang.AutoCleanup
import spock.lang.Specification
class ProfileServiceSpec extends Specification {
@AutoCleanup
ExecHarness execHarness = ExecHarness.harness() <i class="conum" data-value="1"></i><b>(1)</b>
def 'can add/retrieve/remove profiles from service'() {
given:
ProfileService service = new ProfileService()
when:
ExecResult&lt;Promise&lt;List&lt;Profile&gt;&gt;&gt; result = execHarness.yield { service.profiles } <i class="conum" data-value="2"></i><b>(2)</b>
then:
result.value == []
when:
execHarness.yield { service.add(new Profile(role: 'admin', token: 'secret')) }
and:
List&lt;Profile&gt; profiles = execHarness.yield { service.profiles }.value
then: profiles == [new Profile(role: 'admin', token: 'secret')]
when:
execHarness.yield { service.delete() }
then:
execHarness.yield { service.profiles }.value == []
}
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Create an <code>ExecHarness</code> and tell Spock to clean up when we are finished</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Use <code>ExecHarness#yield</code> to wrap all of our service calls so that our Promises and Operations can be resolved on a Ratpack managed thread.</td>
</tr>
</table>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="truefunctional-testing"><a class="anchor" href="#truefunctional-testing"></a>Functional testing</h2>
<div class="sectionbody">
<div class="sect2">
<h3 id="truemainclassapplicationundertest"><a class="anchor" href="#truemainclassapplicationundertest"></a>MainClassApplicationUnderTest</h3>
<div class="dlist">
<dl>
<dt class="hdlist1"><code>GroovyRatpackMainApplicationUnderTest</code></dt>
<dd>
<p>For testing <code>ratpack.groovy</code> backed applications</p>
</dd>
</dl>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy"> @AutoCleanup
@Shared
GroovyRatpackMainApplicationUnderTest groovyScriptApplicationunderTest = new GroovyRatpackMainApplicationUnderTest()</code></pre>
</div>
</div>
<div class="dlist">
<dl>
<dt class="hdlist1"><code>MainClassApplicationUnderTest</code></dt>
<dd>
<p>For testing class backed applications</p>
</dd>
</dl>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy"> @AutoCleanup
@Shared
MainClassApplicationUnderTest mainClassApplicationUnderTest = new MainClassApplicationUnderTest(MainClassApp)</code></pre>
</div>
</div>
<div class="paragraph">
<p>Our sample Ratpack application for testing:</p>
</div>
<div class="listingblock">
<div class="title">ratpack.groovy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import static ratpack.groovy.Groovy.ratpack
ratpack {
serverConfig {
sysProps() <i class="conum" data-value="1"></i><b>(1)</b>
require('/api', ApiConfig) <i class="conum" data-value="2"></i><b>(2)</b>
}
bindings {
bind(ConfService) <i class="conum" data-value="3"></i><b>(3)</b>
}
handlers {
get { ConfService confService -&gt;
confService.conferences.map { <i class="conum" data-value="4"></i><b>(4)</b>
"Here are the best conferences: $it"
} then(context.&amp;render)
}
}
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Pull configuration from System properties</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Create an ApiConfig object and put into the registry</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>Bind <code>ConfService</code> using Guice</td>
</tr>
<tr>
<td><i class="conum" data-value="4"></i><b>4</b></td>
<td>Use <code>ConfService</code> to retrieve list of awesome Groovy Conferences</td>
</tr>
</table>
</div>
<div class="listingblock">
<div class="title">ApiConfig.groovy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">class ApiConfig {
String url
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>Simple object to contain our configuration data related to an API</p>
</div>
<div class="listingblock">
<div class="title">ConfService</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import com.google.inject.Inject
import ratpack.exec.Promise
import ratpack.http.client.HttpClient
class ConfService {
final HttpClient httpClient
final ApiConfig apiConfig
@Inject
ConfService(ApiConfig apiConfig, HttpClient httpClient) { <i class="conum" data-value="1"></i><b>(1)</b>
this.apiConfig = apiConfig
this.httpClient = httpClient
}
Promise&lt;List&lt;String&gt;&gt; getConferences() { <i class="conum" data-value="2"></i><b>(2)</b>
httpClient.get(new URI(apiConfig.url))
.map { it.body }
.map { it.text.split(',').collect { it } }
}
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Receive <code>ApiConfig</code> and <code>HtpClient</code> from Guice</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Define an asynchronous service method to retrieve data from remote service</td>
</tr>
</table>
</div>
</div>
<div class="sect2">
<h3 id="trueconfiguration"><a class="anchor" href="#trueconfiguration"></a>Configuration</h3>
<div class="paragraph">
<p>We can take advantage of system properties to change how the Ratpack application configures its services.</p>
</div>
<div class="listingblock">
<div class="title">FunctionalSpec.groovy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import ratpack.groovy.test.GroovyRatpackMainApplicationUnderTest
import ratpack.groovy.test.embed.GroovyEmbeddedApp
import ratpack.test.ApplicationUnderTest
import ratpack.test.http.TestHttpClient
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
class FunctionalSpec extends Specification {
@Shared
@AutoCleanup
ApplicationUnderTest aut = new GroovyRatpackMainApplicationUnderTest() <i class="conum" data-value="1"></i><b>(1)</b>
@Delegate
TestHttpClient client = aut.httpClient <i class="conum" data-value="2"></i><b>(2)</b>
@Shared
@AutoCleanup
GroovyEmbeddedApp api = GroovyEmbeddedApp.fromHandler { <i class="conum" data-value="3"></i><b>(3)</b>
render 'Greach, GR8conf, Gradle Conf'
}
def setup() {
System.setProperty('ratpack.api.url', api.address.toURL().toString()) <i class="conum" data-value="4"></i><b>(4)</b>
}
def 'can get best conferences'() { <i class="conum" data-value="5"></i><b>(5)</b>
when:
get()
then:
response.statusCode == 200
and:
response.body.text.contains('Greach')
}
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Create our <code>ApplicationUnderTest</code> and tell Spock to clean up when we&#8217;re done</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Retrieve <code>TestHttpClient</code> and make use of <code>@Delegate</code> to make tests very readable</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>Create a simple service that response with a comma separated list of Groovy Conferences</td>
</tr>
<tr>
<td><i class="conum" data-value="4"></i><b>4</b></td>
<td>Set system property to point to our stubbed service</td>
</tr>
<tr>
<td><i class="conum" data-value="5"></i><b>5</b></td>
<td>Write a simple test to assure that our Ratpack app can make a succcessful call to the remote api</td>
</tr>
</table>
</div>
</div>
<div class="sect2">
<h3 id="trueimpositions"><a class="anchor" href="#trueimpositions"></a>Impositions</h3>
<div class="paragraph">
<p><code>Impositions</code> allow a user to provide overrides to various aspects of the Ratpack application bootstrap phase.</p>
</div>
<div class="ulist">
<ul>
<li>
<p><code>ServerConfigImposition</code> allows to override server configuration</p>
</li>
<li>
<p><code>BindingsImposition</code> allows to provide Guice binding overrides</p>
</li>
<li>
<p><code>UserRegistryImposition</code> allows you to provide alternatives for items in the registry</p>
</li>
</ul>
</div>
<div class="listingblock">
<div class="title">ImpositionSpec</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import ratpack.exec.Promise
import ratpack.groovy.test.GroovyRatpackMainApplicationUnderTest
import ratpack.impose.UserRegistryImposition
import ratpack.impose.ImpositionsSpec
import ratpack.test.ApplicationUnderTest
import ratpack.test.http.TestHttpClient
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
import ratpack.guice.Guice
class ImpositionSpec extends Specification {
@Shared
@AutoCleanup
ApplicationUnderTest aut = new GroovyRatpackMainApplicationUnderTest() {
@Override
protected void addImpositions(ImpositionsSpec impositions) { <i class="conum" data-value="1"></i><b>(1)</b>
impositions.add(
UserRegistryImposition.of(Guice.registry {
it.add(new ConfService(null, null) {
Promise&lt;List&lt;String&gt;&gt; getConferences() {
Promise.value(['Greach'])
}
})
}))
}
}
@Delegate
TestHttpClient client = aut.httpClient
def 'can get list of gr8 conferences'() {
when:
get()
then:
response.statusCode == 200
and:
response.body.text.contains('Greach')
}
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Override <code>addImpositions</code> method to provide a <code>UserRegistryImposition</code> that supplies our own dumb implementation of <code>ConfService</code> that does not need to make any network connections</td>
</tr>
</table>
</div>
</div>
<div class="sect2">
<h3 id="trueremotecontrol"><a class="anchor" href="#trueremotecontrol"></a>RemoteControl</h3>
<div class="paragraph">
<p>Authored by Luke Daley; originally for Grails</p>
</div>
<div class="paragraph">
<p>Used to serialize commands to be executed on the <code>ApplicationUnderTest</code></p>
</div>
<div class="listingblock">
<div class="title">build.gradle</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy"> testCompile ratpack.dependency('remote-test')</code></pre>
</div>
</div>
<div class="paragraph">
<p>Here we add a test compile dependency on <code>io.ratpack:ratpack-remote-test</code> which includes a dependency on <code>remote-control</code></p>
</div>
<div class="listingblock">
<div class="title">RemoteControlSpec.groovy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import io.remotecontrol.client.UnserializableResultStrategy
import ratpack.groovy.test.GroovyRatpackMainApplicationUnderTest
import ratpack.guice.BindingsImposition
import ratpack.impose.ImpositionsSpec
import ratpack.remote.RemoteControl
import ratpack.test.ApplicationUnderTest
import ratpack.test.http.TestHttpClient
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
class RemoteControlSpec extends Specification {
@Shared
@AutoCleanup
ApplicationUnderTest aut = new GroovyRatpackMainApplicationUnderTest() {
@Override
protected void addImpositions(ImpositionsSpec impositions) {
impositions.add(BindingsImposition.of {
it.bindInstance RemoteControl.handlerDecorator() <i class="conum" data-value="1"></i><b>(1)</b>
})
}
}
@Delegate
TestHttpClient client = aut.httpClient
ratpack.test.remote.RemoteControl remoteControl = new ratpack.test.remote.RemoteControl(aut, UnserializableResultStrategy.NULL) <i class="conum" data-value="2"></i><b>(2)</b>
def 'should render profiles'() {
when:
get()
then:
response.body.text == '[]'
when:
remoteControl.exec { <i class="conum" data-value="3"></i><b>(3)</b>
get(ProfileService)
.add(new Profile('admin'))
}
and:
get()
then:
response.body.text.startsWith('[{')
}
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>We use <code>BindingsImposition</code> here to add a hook into the running <code>ApplicationUnderTest</code> that allows us to run remote code on the server</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>We tell <code>RemoteControl</code> not to complain if the result of the command is not Serializable</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>We use remote control here to grab the <code>ProfileService</code> and manually add a profile</td>
</tr>
</table>
</div>
</div>
<div class="sect2">
<h3 id="trueephemeralbasedir"><a class="anchor" href="#trueephemeralbasedir"></a>EphemeralBaseDir</h3>
<div class="paragraph">
<p>A utility that provides a nice way to interact with files that would provide the basis of a base directory for Ratpack applications.
It is also an <code>AutoCloseable</code> so you&#8217;ll need to make sure to clean up after use.</p>
</div>
<div class="listingblock">
<div class="title">EphemeralSpec.groovy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import ratpack.groovy.test.embed.GroovyEmbeddedApp
import ratpack.test.embed.EphemeralBaseDir
import spock.lang.Specification
import java.nio.file.Path
class EphemeralSpec extends Specification {
def 'can supply ephemeral basedir'() {
expect:
EphemeralBaseDir.tmpDir().use { baseDir -&gt;
baseDir.write("mydir/.ratpack", "")
baseDir.write("mydir/assets/message.txt", "Hello Ratpack!")
Path mydir = baseDir.getRoot().resolve("mydir")
ClassLoader classLoader = new URLClassLoader((URL[]) [mydir.toUri().toURL()].toArray())
Thread.currentThread().setContextClassLoader(classLoader);
GroovyEmbeddedApp.of { serverSpec -&gt;
serverSpec
.serverConfig { c -&gt; c.baseDir(mydir) }
.handlers { chain -&gt;
chain.files { f -&gt; f.dir("assets") }
}
}.test {
String message = getText("message.txt")
assert "Hello Ratpack!" == message
}
}
}
}</code></pre>
</div>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="trueresources"><a class="anchor" href="#trueresources"></a>Resources</h2>
<div class="sectionbody">
<div class="ulist">
<ul>
<li>
<p><a href="http://shop.oreilly.com/product/0636920037545.do">(book) O&#8217;Reilly: Learning Ratpack by Dan Woods</a></p>
</li>
<li>
<p><a href="https://ratpack.io/manual/current/api/ratpack/test/package-summary.html">(javadocs) Ratpack Test</a></p>
</li>
<li>
<p><a href="https://ratpack.io/manual/current/api/ratpack/groovy/test/package-summary.html">(javadocs) Ratpack Groovy Test</a></p>
</li>
<li>
<p><a href="https://ratpack.io/manual/current/api/ratpack/remote/package-summary.html">(javadocs) Ratpack Remote</a></p>
</li>
<li>
<p><a href="https://ratpack.io/manual/current/api/ratpack/test/remote/package-summary.html">(javadocs) Ratpack Remote Test</a></p>
</li>
</ul>
</div>
</div>
</div>
</div>
<div id="footer">
<div id="footer-text">
Last updated 2016-04-08 22:56:00 EDT
</div>
</div>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.9.1/styles/github.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.9.1/highlight.min.js"></script>
<script>hljs.initHighlighting()</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment