-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.html
724 lines (606 loc) · 51.2 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
<!DOCTYPE html>
<html>
<head>
<title>Steve Smith - Ruby Developer</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>Steve Smith</h1>
</header>
<nav id="menu">
<ul>
<li><a href="#" data-number=1 class="active">About me</a></li>
<li><a href="#" data-number=2>Dev Projects</a></li>
<li><a href="#" data-number=3>CV</a></li>
<li><a href="#" data-number=4>Blog Posts</a></li>
<li><a href="#" data-number=5>Learning Resources</a></li>
</ul>
</nav>
<!--ABOUT ME-->
<div class="content content-1">
<div class="sub-header">
<h2>About Me</h2>
</div>
<div class="site-wrap">
<div class="blurb">
<p>Hi, I'm Steve. After working for almost ten years in software testing / quality assurance (QA) and test automation, I've made the switch to software development. This website is my attempt to showcase myself, stretch my coding muscles, and help others who are looking to get into software or web development.</p>
</div>
<div class="screenshot">
<img src="images/steve.png">
</div>
</div>
</div>
<!--DEV PROJECTS-->
<div class="content content-2 off-left">
<div class="sub-header">
<h2>Dev Projects</h2>
</div>
<div class="intro">
<p>A selection of some of the projects I have been contributing to as a developer in my spare time.</p>
</div>
<div class="site-wrap">
<div class="blurb">
<h3>Draw My Life</h3>
<p>I am one of a small team of engineers who are working on this project, the aim of which is to create a library of drawings made by child refugees, so they may be used to lobby for more funding towards children’s mental health initiatives amongst refugee populations. The app is built on Ruby on Rails, and provides an API in order to provide images and data with a wider audience, including the Humanitarian Data Exchange (HDX).</p>
<p>Github: <a href="https://github.com/empowerhack/DrawMyLife-Service">https://github.com/empowerhack/DrawMyLife-Service</a></p>
</div>
<div class="screenshot">
<img src="images/drawmylife-screenshot.png">
</div>
</div>
<div class="site-wrap">
<div class="blurb">
<h3>ACB Calculator</h3>
<p>The ACB Calculator is a web app (Ruby on Rails) I have created for a student doctor friend of mine. It is a tool used to calculate a patient’s Anticholinergic Burden (ACB) score based on the medicines they are taking. The app uses Javascript and JQuery to update a running score based on medicines entered and warn the user when the score hits a dangerous level. It also uses caching to work offline, as this is necessary in a hospital setting.</p>
<p>Live website: <a href="http://www.acbcalc.com">http://www.acbcalc.com</a></p>
<p>Github: <a href="https://github.com/stevesmith2609/acb-calc">https://github.com/stevesmith2609/acb-calc</a></p>
</div>
<div class="screenshot">
<img src="images/acbcalc-screenshot.png">
</div>
</div>
<div class="site-wrap">
<div class="blurb">
<h3>UK Ultimate</h3>
<p>UK Ultimate is the governing body of Ultimate Frisbee in the UK. I took over the technical running of their website in late 2012, and have been hosting, supporting, and providing patches and updates ever since. It is based on a Drupal framework (PHP), with some custom modules that I have expanded the functionality of. It is used by almost 4,000 members to pay annual fees and to keep up to date with UKU news and events.</p>
<p>Live website: <a href="http://www.ukultimate.com">http://www.ukultimate.com</a></p>
</div>
<div class="screenshot">
<img src="images/ukultimate-screenshot.png">
</div>
</div>
</div>
<!-- CV -->
<div class="content content-3 off-left">
<div class="sub-header">
<h2>CV</h2>
</div>
<div class="intro">
<p>Download a <a href="files/SteveSmithCV.pdf">pdf</a> or <a href="files/SteveSmithCV.doc">Microsoft Word</a> version</p>
</div>
<div class="site-wrap">
<div>
<center><h2>Steve Smith</h2></center>
<h3>
Personal Statement
</h3>
<p>
I’m a hard-working, professional developer, who has recently made the jump from testing (QA). I have over 10 years experience working on software development projects, and am now looking to move into a new position in which I can contribute to awesome projects, and progress my learning, especially in Ruby.
</p>
<hr>
<h3>
Development Experience (professional)
</h3>
<h4>
Full Stack Developer at Appear Here
</h4>
<p>
One of two Full Stack Developers working on the Appear Here product, I am involved in all changes to the <b>Ruby</b> codebase (whether writing or reviewing), and a significant amount of the front-end (<b>React</b>, <b>Javascript</b>) changes. The code I write is clean and well tested in order to meet the high standards of the team.
</p>
<p>
As a full stack developer, I have also had a <b>DevOps</b> role. This involved managing servers on <b>AWS</b>, monitoring and fixing issues within the <b>ETL</b> system, investigating and resolving security concerns, and managing domains and SSL certificates.
</p>
<p>
Projects I’ve contributed towards include:
<ul>
<li>Providing a new search experience for users, becoming particularly familiar with <b>Elasticsearch</b> technologies;</li>
<li>Improving admin functionality based on feedback from admin users;</li>
<li>Importing data from external sources;</li>
<li>Bug fixes and tech support for the wider company.</li>
</ul>
</p>
<h4>
Test Automation at Venntro
</h4>
<p>
As a senior tester, I took responsibility for much of the development and configuration of the existing automated browser-testing suite. I was required to create new tests, update existing ones, fix bugs, refactor inefficient code, configure the CI suite, and onboard new team members so they could use it.
</p><p>
The suite is written in <b>Ruby</b>, on a <b>Capybara</b> framework, utilising <b>Selenium Webdriver</b> to control the browser. Tests were run both manually, and on a schedule from <b>Jenkins</b>, and <b>Git</b> and <b>GitHub</b> were used extensively to work on feature branches, and request and review changes.
</p>
<h4>
Release Report Automation at Lyst
</h4>
<p>
The daily release report took two hours to complete manually each day, so I took it upon myself to use some recent <b>Rails</b> learning to create an app to do the legwork.
</p><p>
The app queried the <b>JIRA API</b> to find information about tickets that had been completed that day, and also the <b>GitHub API</b> to find related pull requests (using <b>regex</b> to find ticket numbers). It reduced the amount of effort required to produce the report each evening down to a couple of minutes of light editing.
</p>
<hr>
<h3>
Development Experience (volunteer)
</h3>
<h4>
Draw My Life - <a href="https://github.com/empowerhack/DrawMyLife-Service">Github Repo</a>
</h4>
<p>
I am one of a small team of engineers who are working on this project, the aim of which is to create a library of drawings made by child refugees, so they may be used to lobby for more funding for children’s mental health amongst refugee populations. The app is built on <b>Ruby on Rails</b>, and provides an <b>API</b> in order to provide images and data with a wider audience, including the Humanitarian Data Exchange (HDX).
</p>
<h4>
ACB Calculator - <a href="https://github.com/stevesmith2609/acb-calc">Github Repo</a> - <a href="http://www.acbcalc.com">Live Site</a>
</h4>
<p>
The ACB Calculator is a web app (<b>Ruby on Rails</b>) I have created for a student doctor friend of mine. It is used to calculate a patient’s Anticholinergic Burden (ACB) score based on their medication. The app uses <b>Javascript</b> to update a running score based on medicines entered and also works as an offline web app, necessary in a hospital setting. It was highly commended in the HSJ Patient Safety Awards 2017.
</p>
<h4>
UK Ultimate - <a href="http://www.ukultimate.com">Live Site</a>
</h4>
<p>
UK Ultimate is the governing body of Ultimate Frisbee in the UK. I took over the technical running of their website in late 2012, and have been hosting, supporting, and providing patches and updates ever since. It is based on a <b>Drupal</b> framework (<b>PHP</b>), with custom modules that I have expanded the functionality of. It is used by almost 4,000 members to pay fees and to keep up to date with news and events.
</p>
<hr>
<h3>
Employment History
</h3>
<p>
Apr 2017 - Present <span class="employment"><b>Full Stack Developer at Appear Here</b></span>
</p>
<p>
Dec 2012 - Apr 2017 <span class="employment"><b>Director and QA Consultant of Ultimate Testing Ltd</b></span>
</p>
<p>
<span class="employment">Contract roles at <b>Venntro</b> (QA and Test Automation), <b>Tribal Worldwide</b> (Test Management), and <b>BMT Defence Services</b> (QA)</span>
<br><br>
</p>
<p>
May 2015 - Sep 2015 <span class="employment"><b>QA Engineer at Lyst</b></span>
</p>
<p>
Jul 2011 - Jun 2012 <span class="employment"><b>Senior Consultant at Deloitte</b></span>
</p>
<p>
Aug 2010 - Jul 2011 <span class="employment"><b>Test Analyst at Serco (businesslink.gov.uk)</b></span>
</p>
<p>
Sep 2007 - Aug 2010 <span class="employment"><b>IT Graduate / Test Analyst at AXA</b></span>
</p>
<hr>
<h3>Education</h3>
<p>
2004 - 2007 <span class="employment"><b>BSc Accounting, 2:1 classification</b><br>Cardiff University</span><br><br>
</p>
<p>
1997 - 2004 <span class="employment"><b>4 ‘A’ Levels, 1 ‘AS’ Level, 10 GCSEs</b><br>DHSB, Plymouth</span><br><br>
</p>
<hr>
<h3>Interests and Hobbies</h3>
<p>
I enjoy playing Ultimate Frisbee, a non-contact team sport I picked up at university, and have been playing ever since. It is great exercise, and I really enjoy being part of a team. I play for Thundering Herd, a mixed-gender team based in Clapham. I am also a keen traveller, both for Frisbee tournaments, and for my own pleasure.
</p>
</div>
</div>
</div>
<!-- BLOG POSTS -->
<div class="content content-4 off-left">
<div class="sub-header">
<h2>Blog Posts</h2>
</div>
<div class="intro">
<ul>
<li>
<a href="#slackreporting">Reporting Cucumber Results in Slack</a> - November 2016
</li>
<li>
<a href="#commandline">Test Automation: Taking Command of the Command Line</a> - October 2016
</li>
</ul>
</div>
<div class="site-wrap">
<a name="slackreporting"></a>
<div>
<p>
Originally posted at <a href="http://dev.venntro.com/2016/11/reporting-cucumber-results-in-slack/">http://dev.venntro.com/2016/11/reporting-cucumber-results-in-slack/</a>
</p>
<h2>Reporting Cucumber Results in Slack</h2>
<p>Examples from this blog post currently run on a front-end automation suite running Ruby 2.2.0.</p>
<p>Increasingly, we’ve moved the running of our automation testing from our development machines to an
instance of <a href="https://jenkins.io/">Jenkins</a> living on a dedicated CI server. This frees up our development
machines (and their limited capacity) for us to use in manual testing, exploratory testing and test
automation development. It also gives us a consistent environment from which to run tests, and
the ability to schedule testing overnight while the test environments are not being used.</p>
<p>Although it has its advantages, this does somewhat create an undesired separation between our
testers and the test results being reported for them. As we are a development team that loves using
<a href="https://slack.com/">Slack</a>, we’ve created a way of reporting these test results directly to our chosen messaging
app. I’ll take you through how we’ve accomplished this for our regular Cucumber test runs and for
more advanced Parallel Cucumber test runs.</p>
<h2 id="cucumber">Cucumber</h2>
<p><i>Using the <a href="https://rubygems.org/gems/cucumber">cucumber gem</a></i></p>
<p>We want to report our results at the scenario level, as feature results are too high-level, and
individual step results are too low-level. My original thought was to parse the json output from the
Cucumber gem to find the results I required, but as the json output works only at the step level, it
would be time-consuming to create a tool to translate this into scenario results. Fortunately, the
html output of cucumber already does this for us.</p>
<p>The standard html output from Cucumber has this header:</p>
<p><img src="images/blog/cucumber_html.png" alt="Header of html output of Cucumber gem" style="width: 717px; height: 67px;" /></p>
<p>I’ve found it’s a lot easier to parse the html of this output to grab the scenario statistics from
the top line, “27 scenarios (2 failed, 1 skipped, 24 passed)” than it is to infer it from hundreds
of individual step results.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">get_run_stats_standard</span><span class="p">(</span><span class="n">report</span><span class="p">)</span>
<span class="n">stats</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">passed: </span><span class="mi">0</span><span class="p">,</span> <span class="ss">failed: </span><span class="mi">0</span><span class="p">,</span> <span class="ss">skipped: </span><span class="mi">0</span><span class="p">,</span> <span class="ss">undefined: </span><span class="mi">0</span> <span class="p">}</span>
<span class="n">file</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="n">report</span><span class="p">)</span>
<span class="n">scenarios_line</span> <span class="o">=</span> <span class="n">file</span><span class="p">.</span><span class="nf">lines</span><span class="p">.</span><span class="nf">last</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="s1">'innerHTML = "'</span><span class="p">).</span><span class="nf">last</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="s1">'<br />'</span><span class="p">).</span><span class="nf">first</span>
<span class="n">stats</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">state</span><span class="o">|</span>
<span class="k">if</span> <span class="n">scenarios_line</span><span class="p">.</span><span class="nf">include?</span> <span class="n">state</span><span class="p">.</span><span class="nf">to_s</span>
<span class="n">stats</span><span class="p">[</span><span class="n">state</span><span class="p">]</span> <span class="o">=</span> <span class="n">scenarios_line</span><span class="p">[</span><span class="sr">/(\d+) </span><span class="si">#{</span><span class="n">state</span><span class="p">.</span><span class="nf">to_s</span><span class="si">}</span><span class="sr">/</span><span class="p">,</span> <span class="mi">1</span><span class="p">].</span><span class="nf">to_i</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>We set all stats to 0 before reading the output generated by Cucumber and extracting the first line
of text from the last line in the file (although it is in the header, the statistics are actually in
the last line of the html file). <code class="highlighter-rouge">scenarios_line</code> in this instance equals <code class="highlighter-rouge">"27 scenarios (2 failed,
1 skipped, 24 passed)"</code>. For each of the states that the scenario could be (passed, failed, skipped,
and undefined*), we run a check to see if it occurs in the string; if it does, we grab the number
from the string using regex, and if it doesn’t, we leave it at 0.</p>
<p>The output of this is a handy hash of the scenario results:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="p">{</span> <span class="ss">passed: </span><span class="mi">24</span><span class="p">,</span> <span class="ss">failed: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">skipped: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">undefined: </span><span class="mi">0</span> <span class="p">}</span></code></pre></figure>
<p>* In cases where a step in a feature file is not defined in any step definition file, although the
scenario is skipped, it is reported as “undefined”.</p>
<h2 id="parallel-cucumber">Parallel Cucumber</h2>
<p><i>Using the <a href="https://rubygems.org/gems/parallel_tests">parallel_tests gem</a> and <a href="https://rubygems.org/gems/report_builder">report_builder gem</a><i></i></i></p>
<p>The standard output of report_builder gem is a rich html file to display results:</p>
<p><img src="images/blog/parallel_html.png" alt="HTML output of report_builder gem" style="max-width:100%;" /></p>
<p>This is nice to look at, and we <i>could</i> parse the html as before, but we are also provided with an
exceptionally helpful array of statistics, which is perfect for our use case. Here is an example of
the result of running <code class="highlighter-rouge">output = ReportBuilder.build_report</code> on a recent run:</p>
<p><pre>
[
288583677059,
[
{:name=>"broken", :count=>1, :color=>"#f45b5b"},
{:name=>"incomplete", :count=>1, :color=>"#e7a35c"}
],
[
{:name=>"failed", :count=>1, :color=>"#f45b5b"},
{:name=>"passed", :count=>6, :color=>"#90ed7d"},
{:name=>"skipped", :count=>1, :color=>"#7cb5ec"},
{:name=>"undefined", :count=>1, :color=>"#e4d354"}
],
[
{:name=>"passed", :count=>68, :color=>"#90ed7d"},
{:name=>"failed", :count=>1, :color=>"#f45b5b"},
{:name=>"skipped", :count=>5, :color=>"#7cb5ec"},
{:name=>"undefined", :count=>1, :color=>"#e4d354"}
]
]
</pre></p>
<p>After the first value of this array (duration in nanoseconds), the second value is an array of
feature results, the third is scenario results in an array, and the fourth is an array of individual
step results.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">get_run_stats_parallel</span><span class="p">(</span><span class="n">report</span><span class="p">)</span>
<span class="n">stats</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">passed: </span><span class="mi">0</span><span class="p">,</span> <span class="ss">failed: </span><span class="mi">0</span><span class="p">,</span> <span class="ss">skipped: </span><span class="mi">0</span><span class="p">,</span> <span class="ss">undefined: </span><span class="mi">0</span> <span class="p">}</span>
<span class="n">stats</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">state</span><span class="o">|</span>
<span class="k">unless</span> <span class="n">report</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nf">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">status</span><span class="o">|</span> <span class="n">status</span><span class="p">[</span><span class="ss">:name</span><span class="p">]</span> <span class="o">==</span> <span class="n">state</span><span class="p">.</span><span class="nf">to_s</span> <span class="p">}.</span><span class="nf">empty?</span>
<span class="n">stats</span><span class="p">[</span><span class="n">state</span><span class="p">]</span> <span class="o">=</span> <span class="n">report</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nf">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">status</span><span class="o">|</span> <span class="n">status</span><span class="p">[</span><span class="ss">:name</span><span class="p">]</span> <span class="o">==</span> <span class="n">state</span><span class="p">.</span><span class="nf">to_s</span> <span class="p">}.</span><span class="nf">first</span><span class="p">[</span><span class="ss">:count</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>As before, we set all results to 0 by default, then for each of the possible scenario states, check
the third value of the report to see if any hash within it has a <code class="highlighter-rouge">:name</code> matching the state. If not,
we leave the count as 0, and if so, we grab the <code class="highlighter-rouge">:count</code> value of that hash.</p>
<p>The result is a now-familiar hash:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="p">{</span> <span class="ss">passed: </span><span class="mi">6</span><span class="p">,</span> <span class="ss">failed: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">skipped: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">undefined: </span><span class="mi">1</span> <span class="p">}</span></code></pre></figure>
<h2 id="sending-results-to-slack">Sending Results to Slack</h2>
<p>Once we have extracted the results, and manipulated them into a human readable format, we use the
Slack API to send them to Slack. I won’t go into the full details of how to do this, as the API is
<a href="https://api.slack.com/">well documented by Slack</a>, but this method will get you most of the way there:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">post_to_slack</span><span class="p">(</span><span class="n">msg_text</span><span class="p">)</span>
<span class="n">uri</span> <span class="o">=</span> <span class="no">URI</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="s2">"https://slack.com/api/chat.postMessage"</span><span class="p">)</span>
<span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="p">.</span><span class="nf">post_form</span><span class="p">(</span><span class="n">uri</span><span class="p">,</span> <span class="p">{</span>
<span class="s2">"token"</span> <span class="o">=></span> <span class="no">SLACK_TOKEN</span><span class="p">,</span>
<span class="s2">"channel"</span> <span class="o">=></span> <span class="no">SLACK_CHANNEL</span><span class="p">,</span>
<span class="s2">"attachments"</span> <span class="o">=></span> <span class="p">[{</span>
<span class="ss">text: </span><span class="n">msg_text</span><span class="p">,</span>
<span class="ss">fallback: </span><span class="n">msg_text</span><span class="p">,</span>
<span class="ss">color: </span><span class="s2">"good"</span><span class="p">,</span>
<span class="ss">mrkdwn_in: </span><span class="p">[</span><span class="s2">"text"</span><span class="p">,</span> <span class="s2">"fallback"</span><span class="p">]</span>
<span class="p">}].</span><span class="nf">to_json</span><span class="p">,</span>
<span class="s2">"link_names"</span> <span class="o">=></span> <span class="mi">1</span><span class="p">,</span>
<span class="s2">"username"</span> <span class="o">=></span> <span class="no">SLACK_USERNAME</span><span class="p">,</span>
<span class="s2">"as_user"</span> <span class="o">=></span> <span class="kp">false</span><span class="p">,</span>
<span class="s2">"icon_url"</span> <span class="o">=></span> <span class="no">LOGO_URL</span>
<span class="p">})</span>
<span class="k">end</span></code></pre></figure>
<p>Obviously, you’ll need to provide your own values for <code class="highlighter-rouge">SLACK_TOKEN</code>, <code class="highlighter-rouge">SLACK_CHANNEL</code>,
<code class="highlighter-rouge">SLACK_USERNAME</code>, and <code class="highlighter-rouge">LOGO_URL</code>, depending on your own implementation of Slack. For
<code class="highlighter-rouge">SLACK_CHANNEL</code>, you could even have it defined as a Project Parameter within Jenkins as we do, so
that users can choose their own channel to report to when they kick off a build.</p>
<h2 id="results">Results</h2>
<p>Each night, we run our full automation suite of almost 400 test scenarios against our two staging
environments. We run them in three batches:</p>
<ol>
<li>
<p>We run as many as possible using the parallel_tests gem, running 4 streams of test simultaneously
for speed. We record all failures in a txt file.</p>
</li>
<li>
<p>We re-run the failing scenarios from the first run using the standard cucumber gem (i.e.
sequentially). Most of the time, failures identified in the first run happen because a simultaneous
test has disrupted the test environment configuration required by the test.</p>
</li>
<li>
<p>We run a final set of scenarios through the standard cucumber gem sequentially. These are
scenarios which require the environment to be configured in a very specific way, and so have been
<a href="https://github.com/cucumber/cucumber/wiki/Tags">tagged</a> specifically for their own sequential test run.</p>
</li>
</ol>
<p>The results of each are fed through their respective <code class="highlighter-rouge">get_run_stats_*</code> method, amalgamated, and then
sent to Slack using the method above to come up with our nightly report:</p>
<p><img src="images/blog/slackstats.png" alt="Results of our nightly CI run displayed in Slack" style="display: block; margin: auto; width: 600px; height: 265px;" /></p>
<p>In the morning following this run, a designated member of the team will investigate these failures,
report on the cause (whether it be environmental, a bug in the tests, or a bug on one of the
branches deployed to that staging environment), and take any further action as necessary to fix the
bugs or improve reliability of the test suite.</p>
<p>However, it’s not just the large nightly runs that can take advantage of these methods; we send the
results of all Jenkins builds through this code, so anyone can be given an instantaneous glimpse of
how any run they’ve kicked off has done, however small.</p>
<p><img src="images/blog/slackstats-sm.png" alt="Results of a custom automation run displayed in Slack" style="display: block; margin: auto; width: 600px; height: 95px;" /></p>
</div>
</div>
<div class="site-wrap">
<a name="commandline"></a>
<div>
<p>
Originally posted at <a href="http://dev.venntro.com/2016/10/test-automation-taking-command-of-the-command-line/">http://dev.venntro.com/2016/10/test-automation-taking-command-of-the-command-line/</a>
</p>
<h2>Test Automation: Taking Command of the Command Line</h2>
<p>Examples from this blog post currently run on a front-end automation suite
running Ruby 2.2.0. <a href="https://jenkins.io/">Jenkins</a> is used as a CI server to run tests from.</p>
<h2 id="background">Background</h2>
<p>The automation test suite here at Venntro has been growing steadily over the
past few years, and is now able to test most of the user-facing features on the
platform. However, there are some features which have always been difficult for
us to automate tests for.</p>
<p>Search and member feeds (which members we recommend to the user on different
parts of the site), have been notoriously difficult because once you make
changes to the data, they need to be reindexed by the
<a href="http://sphinxsearch.com/">Sphinx search server</a> before changes are actually effected on the
front-end. This runs every few minutes on the test server, but as any test
automation specialist will tell you, a few seconds is too long for a test to be
waiting, let alone a few minutes!</p>
<p>We needed to be able to force this index to be rebuilt, which required us to be
able to access the “scary world” of the command line on the test environment
host server.</p>
<h2 id="evolution">Evolution</h2>
<h3 id="first-iteration-basic-commands">First Iteration: Basic Commands</h3>
<p>It turns out Ruby has <a href="http://stackoverflow.com/questions/7212573/when-to-use-each-method-of-launching-a-subprocess-in-ruby/7263556">many ways to access the command line built-in</a>,
and the most simple way to do this was to just encase the command we wanted to
run in backticks:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">reindex_all_sphinx_indices</span> <span class="k">do</span>
<span class="sb">`ssh subdomain.hostname.com 'sudo /usr/bin/indexer --rotate --all'`</span>
<span class="k">end</span></code></pre></figure>
<p>One of the documented disadvantages of using this method is that it blocks
processing until the command is complete. Thankfully, for a test suite, this
actually becomes an advantage; we don’t actually want the test to continue
running until the indexing is complete.</p>
<h3 id="second-iteration-combatting-stalls">Second Iteration: Combatting Stalls</h3>
<p>It didn’t take long using this method before tests started to get stuck at this
point. Unfortunately, there are many things that can happen when using the
command line to cause your test to stall. For example, our script uses a sudo
command, and if the user running the test does not have sudo access for the
action specified, the command line will prompt for a password. As this task is
being run in the background by Ruby, you will never see this prompt.</p>
<p>To combat stalling we reused a tactic from our front-end tests, wrapping the
call in a simple timeout:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">reindex_all_sphinx_indices</span> <span class="k">do</span>
<span class="no">Timeout</span><span class="o">::</span><span class="n">timeout</span><span class="p">(</span><span class="mi">30</span><span class="p">)</span> <span class="k">do</span>
<span class="sb">`ssh subdomain.hostname.com 'sudo /usr/bin/indexer --rotate --all'`</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>So, if the call takes more than 30 seconds, the test will abort as a failure, and
the cause for it taking so long can be investigated.</p>
<h3 id="third-iteration-output">Third Iteration: Output</h3>
<p>Debugging was the next issue to address. It was great that we now had the
ability to run command line operations in our test suite, and a way to deal with
the most crippling issue, stalling, but there were other issues that needed to
be debugged. If a test failed for a missed expectation after appearing to have
executed the command without fail, we had no way to see what had actually
happened during the call.</p>
<p>The command to rotate all the Sphinx indices is an interesting one to inspect.
It runs through each Sphinx index, performs the rotation, and moves on to the
next regardless of whether it successfully performed the rotation or not. Even
if a number of the rotations fail, and these failures are shown in the output to
the user, once the command is completed, Ruby does not understand these fails.
As far as it can tell, the command was completed successfully.</p>
<p>It turns out getting hold of this output is as easy as assigning it to a
variable, so that’s what I did, and immediately output it using the <code class="highlighter-rouge">puts</code>
method.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">reindex_all_sphinx_indices</span> <span class="k">do</span>
<span class="no">Timeout</span><span class="o">::</span><span class="n">timeout</span><span class="p">(</span><span class="mi">30</span><span class="p">)</span> <span class="k">do</span>
<span class="n">output</span> <span class="o">=</span> <span class="sb">`ssh subdomain.hostname.com 'sudo /usr/bin/indexer --rotate --all'`</span>
<span class="nb">puts</span> <span class="n">output</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>Not long after implementing this functionality, our test environment was rebuilt
on a new server. By seeing this output in our test automation console output, I
could alert our Operations team to the fact that the Sphinx user being used did
not have access to a specific database, which was causing the index rotation to
quietly fail unbeknownst to anyone not closely monitoring the logs.</p>
<p>This worked for some specific scenarios, but one of the review comments I got
from my colleague when I proposed it was:</p>
<p><img src="images/blog/mmoore-comment.png" alt="Matt points out this will not work if the call times out" style="width: 692px; height: 135px; display: block; margin: auto;" /></p>
<p>I tested this by reducing the timeout down to a few seconds, and sure enough
found that when using this backticks methods, the output will only be written to
the variable <code class="highlighter-rouge">output</code> on completion of the call. Even if the timeout does its
job, and <code class="highlighter-rouge">puts output</code> is moved to a rescue block, the variable <code class="highlighter-rouge">output</code> will be
empty.</p>
<p>It was time to dig deeper into Ruby’s array of command line tools.</p>
<h3 id="fourth-iteration-iopipes">Fourth Iteration: IO.pipes</h3>
<p>After a bit of research into the challenge, I stumbled across an awesome
<a href="http://stackoverflow.com/a/31465248">StackOverflow answer</a>, which gave a fantastic basis for what
would eventually be the method we use to execute command line operations:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">exec_with_timeout</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">timeout</span> <span class="o">=</span> <span class="mi">30</span><span class="p">)</span>
<span class="n">rout</span><span class="p">,</span> <span class="n">wout</span> <span class="o">=</span> <span class="no">IO</span><span class="p">.</span><span class="nf">pipe</span>
<span class="n">rerr</span><span class="p">,</span> <span class="n">werr</span> <span class="o">=</span> <span class="no">IO</span><span class="p">.</span><span class="nf">pipe</span>
<span class="n">stdout</span><span class="p">,</span> <span class="n">stderr</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="k">begin</span>
<span class="nb">puts</span> <span class="s2">"Executing: </span><span class="si">#{</span><span class="n">cmd</span><span class="si">}</span><span class="s2">"</span>
<span class="n">pid</span> <span class="o">=</span> <span class="no">Process</span><span class="p">.</span><span class="nf">spawn</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="ss">pgroup: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">:out</span> <span class="o">=></span> <span class="n">wout</span><span class="p">,</span> <span class="ss">:err</span> <span class="o">=></span> <span class="n">werr</span><span class="p">)</span>
<span class="no">Timeout</span><span class="p">.</span><span class="nf">timeout</span><span class="p">(</span><span class="n">timeout</span><span class="p">)</span> <span class="k">do</span>
<span class="no">Process</span><span class="p">.</span><span class="nf">waitpid</span><span class="p">(</span><span class="n">pid</span><span class="p">)</span>
<span class="n">wout</span><span class="p">.</span><span class="nf">close</span>
<span class="n">werr</span><span class="p">.</span><span class="nf">close</span>
<span class="n">stdout</span> <span class="o">=</span> <span class="n">rout</span><span class="p">.</span><span class="nf">readlines</span><span class="p">.</span><span class="nf">join</span>
<span class="n">stderr</span> <span class="o">=</span> <span class="n">rerr</span><span class="p">.</span><span class="nf">readlines</span><span class="p">.</span><span class="nf">join</span>
<span class="k">end</span>
<span class="k">rescue</span> <span class="no">Timeout</span><span class="o">::</span><span class="no">Error</span>
<span class="no">Process</span><span class="p">.</span><span class="nf">kill</span><span class="p">(</span><span class="s1">'TERM'</span><span class="p">,</span> <span class="n">pid</span><span class="p">)</span>
<span class="no">Process</span><span class="p">.</span><span class="nf">detach</span><span class="p">(</span><span class="n">pid</span><span class="p">)</span>
<span class="n">wout</span><span class="p">.</span><span class="nf">close</span>
<span class="n">werr</span><span class="p">.</span><span class="nf">close</span>
<span class="n">timedout</span> <span class="o">=</span> <span class="kp">true</span>
<span class="n">stdout</span> <span class="o">=</span> <span class="n">rout</span><span class="p">.</span><span class="nf">readlines</span><span class="p">.</span><span class="nf">join</span>
<span class="n">stderr</span> <span class="o">=</span> <span class="n">rerr</span><span class="p">.</span><span class="nf">readlines</span><span class="p">.</span><span class="nf">join</span>
<span class="k">ensure</span>
<span class="n">wout</span><span class="p">.</span><span class="nf">close</span> <span class="k">unless</span> <span class="n">wout</span><span class="p">.</span><span class="nf">closed?</span>
<span class="n">werr</span><span class="p">.</span><span class="nf">close</span> <span class="k">unless</span> <span class="n">werr</span><span class="p">.</span><span class="nf">closed?</span>
<span class="n">rout</span><span class="p">.</span><span class="nf">close</span>
<span class="n">rerr</span><span class="p">.</span><span class="nf">close</span>
<span class="nb">puts</span> <span class="s2">"OUTPUT: </span><span class="si">#{</span><span class="n">stdout</span><span class="si">}</span><span class="s2">"</span> <span class="k">if</span> <span class="n">stdout</span><span class="p">.</span><span class="nf">present?</span>
<span class="nb">puts</span> <span class="s2">"ERRORS: </span><span class="si">#{</span><span class="n">stderr</span><span class="si">}</span><span class="s2">"</span> <span class="k">if</span> <span class="n">stderr</span><span class="p">.</span><span class="nf">present?</span>
<span class="k">raise</span> <span class="s2">"Execution timed out: </span><span class="si">#{</span><span class="n">cmd</span><span class="si">}</span><span class="s2">"</span> <span class="k">if</span> <span class="n">timedout</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>There’s quite a lot going on, so I’ll explain piece by piece.</p>
<h4 id="the-setup-and-begin-block">The Setup and Begin block</h4>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">rout</span><span class="p">,</span> <span class="n">wout</span> <span class="o">=</span> <span class="no">IO</span><span class="p">.</span><span class="nf">pipe</span>
<span class="n">rerr</span><span class="p">,</span> <span class="n">werr</span> <span class="o">=</span> <span class="no">IO</span><span class="p">.</span><span class="nf">pipe</span>
<span class="n">stdout</span><span class="p">,</span> <span class="n">stderr</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="k">begin</span>
<span class="nb">puts</span> <span class="s2">"Executing: </span><span class="si">#{</span><span class="n">cmd</span><span class="si">}</span><span class="s2">"</span>
<span class="n">pid</span> <span class="o">=</span> <span class="no">Process</span><span class="p">.</span><span class="nf">spawn</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="ss">pgroup: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">:out</span> <span class="o">=></span> <span class="n">wout</span><span class="p">,</span> <span class="ss">:err</span> <span class="o">=></span> <span class="n">werr</span><span class="p">)</span>
<span class="no">Timeout</span><span class="p">.</span><span class="nf">timeout</span><span class="p">(</span><span class="n">timeout</span><span class="p">)</span> <span class="k">do</span>
<span class="no">Process</span><span class="p">.</span><span class="nf">waitpid</span><span class="p">(</span><span class="n">pid</span><span class="p">)</span>
<span class="n">wout</span><span class="p">.</span><span class="nf">close</span>
<span class="n">werr</span><span class="p">.</span><span class="nf">close</span>
<span class="n">stdout</span> <span class="o">=</span> <span class="n">rout</span><span class="p">.</span><span class="nf">readlines</span><span class="p">.</span><span class="nf">join</span>
<span class="n">stderr</span> <span class="o">=</span> <span class="n">rerr</span><span class="p">.</span><span class="nf">readlines</span><span class="p">.</span><span class="nf">join</span>
<span class="k">end</span></code></pre></figure>
<p>We start by creating two <a href="https://ruby-doc.org/core-2.2.0/IO.html#method-c-pipe">IO pipes</a>. One will record STDOUT (standard
output stream from the command), and one will record STDERR (standard error
stream). We then set variables <code class="highlighter-rouge">stdout</code> and <code class="highlighter-rouge">stderr</code> to nil.</p>
<p>After printing out the command being executed, we <a href="https://ruby-doc.org/core-2.2.0/Process.html#method-c-spawn">spawn a new Ruby Process</a>
to run the command given. We make a new process group to run it using <code class="highlighter-rouge">pgroup: true</code>,
and assign the write ends of our two new IO.pipes to record the output and error
streams. We store the process number as variable <code class="highlighter-rouge">pid</code>.</p>
<p>Next, we start our timeout block and use <code class="highlighter-rouge">Process.waitpid(pid)</code> to wait for the
process we spawned to stop. If the time taken to do this does not exceed the
number of seconds specified (30 by default), we close the two write ends of the
IO.pipes (we cannot read from the read ends if not), and then read the read ends
of each pipe, assigning them to the specified variables to print out later.</p>
<h4 id="the-rescue-block">The Rescue block</h4>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">rescue</span> <span class="no">Timeout</span><span class="o">::</span><span class="no">Error</span>
<span class="no">Process</span><span class="p">.</span><span class="nf">kill</span><span class="p">(</span><span class="s1">'TERM'</span><span class="p">,</span> <span class="n">pid</span><span class="p">)</span>
<span class="no">Process</span><span class="p">.</span><span class="nf">detach</span><span class="p">(</span><span class="n">pid</span><span class="p">)</span>
<span class="n">wout</span><span class="p">.</span><span class="nf">close</span>
<span class="n">werr</span><span class="p">.</span><span class="nf">close</span>
<span class="n">timedout</span> <span class="o">=</span> <span class="kp">true</span>
<span class="n">stdout</span> <span class="o">=</span> <span class="n">rout</span><span class="p">.</span><span class="nf">readlines</span><span class="p">.</span><span class="nf">join</span>
<span class="n">stderr</span> <span class="o">=</span> <span class="n">rerr</span><span class="p">.</span><span class="nf">readlines</span><span class="p">.</span><span class="nf">join</span></code></pre></figure>
<p>If the Begin block does time out, the rescue block will clean up. Firstly, it
will terminate and detach the process we started so it does not either become a
zombie process or affect any other tests unexpectedly. Then a similar process of
closing and reading the IO pipes is performed. We also set a local variable,
<code class="highlighter-rouge">timedout</code> to <code class="highlighter-rouge">true</code> for use in error reporting later.</p>
<h4 id="the-ensure-block">The Ensure block</h4>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">ensure</span>
<span class="n">wout</span><span class="p">.</span><span class="nf">close</span> <span class="k">unless</span> <span class="n">wout</span><span class="p">.</span><span class="nf">closed?</span>
<span class="n">werr</span><span class="p">.</span><span class="nf">close</span> <span class="k">unless</span> <span class="n">werr</span><span class="p">.</span><span class="nf">closed?</span>
<span class="n">rout</span><span class="p">.</span><span class="nf">close</span>
<span class="n">rerr</span><span class="p">.</span><span class="nf">close</span>
<span class="nb">puts</span> <span class="s2">"OUTPUT: </span><span class="si">#{</span><span class="n">stdout</span><span class="si">}</span><span class="s2">"</span> <span class="k">if</span> <span class="n">stdout</span><span class="p">.</span><span class="nf">present?</span>
<span class="nb">puts</span> <span class="s2">"ERRORS: </span><span class="si">#{</span><span class="n">stderr</span><span class="si">}</span><span class="s2">"</span> <span class="k">if</span> <span class="n">stderr</span><span class="p">.</span><span class="nf">present?</span>
<span class="k">raise</span> <span class="s2">"Execution timed out: </span><span class="si">#{</span><span class="n">cmd</span><span class="si">}</span><span class="s2">"</span> <span class="k">if</span> <span class="n">timedout</span>
<span class="k">end</span></code></pre></figure>
<p>The ensure block is run no matter the outcome of the Begin or Rescue blocks, and
simply closes any ends of IO pipes that may still be open, and then prints out
all the output and all the errors to the console if they exist.</p>
<p>Finally, a <code class="highlighter-rouge">raise</code> is performed if the rescue block was run. This will
immediately fail the test being run, with a very clear message; this particular
command being run took an unexpectedly long time to complete. This, along with
all the output and errors that were written right up to the time of failure or
timeout should give all the information needed to analyse the output.</p>
<h2 id="hints--tips">Hints & Tips</h2>
<h3 id="access">Access</h3>
<p>When running commands on a remote host, whether you have access to run those
commands is going to be an issue. You should investigate ssh access, tty, known
hosts, and use of the sudo command; you will probably need to have a discussion
with your Ops team to discuss which commands need running, by which users, and
how to do that in a secure way. I found a useful way to ensure if your CI user
has access to run a command was to ssh to the CI server as the user which runs
automation (in our case, <a href="https://jenkins.io/">Jenkins</a> uses the default user ‘jenkins’) and trying
to run the command.</p>
<h3 id="test-candidates">Test candidates</h3>
<p>Any test which requires waiting for a cron job to do something is a prime
candidate for accessing the command line and kicking off the process yourself,
as cron jobs do not by default run at intervals smaller than 1 minute. I’ve also
found that if you need to hit a url in order to trigger an action, but don’t
want your test to navigate away from the page you’re currently on, using this
method to run a <code class="highlighter-rouge">curl</code> command to hit the url works perfectly.</p>
<h3 id="tagging-scenarios">Tagging scenarios</h3>
<p>We use <a href="https://github.com/cucumber/cucumber">Cucumber</a> to organise and execute our test scenarios, which supports
<a href="https://github.com/cucumber/cucumber/wiki/Tags">tagging</a>. We tag scenarios depending on which command line operations are run
during the test, for example, <code class="highlighter-rouge">@ssh</code>, so they can be run as a batch if required,
or excluded from test runs if we are experiencing issues with ssh.</p>
<h3 id="conditionally-printed-output">Conditionally printed output</h3>
<p>If you don’t want all the output printed on every test run, with a bit of
tweaking you could:</p>
<ul>
<li>Only print the output if the call fails or the timeout is exceeded</li>
<li>Add an additional argument to the <code class="highlighter-rouge">exec_with_timeout</code> method to control
whether output is printed or not</li>
<li>Create an environment variable to control whether output is printed or not</li>
</ul>
</div>
</div>
</div>
<!-- LEARNING RESOURCES -->
<div class="content content-5 off-left">
<div class="sub-header">
<h2>Learning Resources</h2>
</div>
<div class="intro">
<p>In my journey to become a developer, I have followed several online courses; the following are those I would highly recommend for others.</p>
</div>
<div class="site-wrap">
<div class="blurb">
<h3>12 in 12 Ruby on Rails Challenge</h3>
<p><a href="https://twitter.com/mackenziechild">Mackenzie Child's</a> 12 in 12 Ruby on Rails Challenge is a course of 12 challenges to build a fully functional web app using Rails. The intention is to complete one challenge each week. The first challenge takes about 6 hours, but stick with it, and as you become more competent with Rails, it'll reduce until you can keep up pace with Mackenzie's walkthroughs.</p>
<p>Videos: <a href="https://www.youtube.com/playlist?list=PL23ZvcdS3XPLNdRYB_QyomQsShx59tpc-">https://www.youtube.com/playlist?list=PL23ZvcdS3XPLNdRYB_QyomQsShx59tpc-</a></p>
</div>
<div class="screenshot">
<img src="images/12in12challenge.png">
</div>
</div>
<div class="site-wrap">
<div class="blurb">
<h3>Javascript30</h3>
<p>Javascript30 is a course of 30 javascript-based "things to build" created by <a href="https://twitter.com/wesbos">Wes Bos</a>. Each challenge takes about 30 minutes to complete, and teaches vanilla javascript rather than relying on other frameworks or libraries. A lot of what I've learnt through this course has helped me with the javascript and styling on this website.</p>
<p>Website: <a href="http://www.javascript30.com">http://www.javascript30.com</a></p>
</div>
<div class="screenshot">
<img src="images/javascript30.png">
</div>
</div>
</div>
<script src="scripts.js"></script>
</body>