layout: true class: title name: title ??? --- layout: true class: content name: content --- layout: true class: content, level-1 name: level-1 --- layout: true class: content, level-2 name: level-2 --- layout: true class: content, level-3 name: level-3 --- layout: true class: image name: image --- layout: true class: bg-cover, image name: full-screen --- layout: true class: double-wide name: double-wide --- layout: true class: double-stack name: double-stack --- layout: true class: code name: code --- layout: true class: title, conversation name: conversation --- layout: true name: cover-art template: title background-image: url(images/title.jpg) class: bg-cover, inverse, cover-art, no-footer --- layout: true template: full-screen name: our-journey class: our-journey, square, no-footer background-image: url(images/our-journey-guidance.jpg) .contents[ .climber[  ] ]
Your browser does not support the
audio
element.
--- layout: false template: cover-art # Unit Testing Your React App .about.right[ ## Steven Hicks ### [@pepopowitz](http://twitter.com/pepopowitz)
### steven.j.hicks@gmail.com
### [stevenhicks.me/react-testing](https://stevenhicks.me/react-testing)
] ??? **Contact Info** **Thanks:** * you! timekillers: * STICKERS! * advice for new people * favorite talks * favorite speakers * stand up & stretch * high fives checklist: * slides sync'ed * audio works * recording audio * recording video * **CLAP!** * favorite kinds of talks * technical * experiential * soft skills * where from? * poll about intro --- template: full-screen background-image: url(images/thatorganizers.jpg) class: no-footer ??? organizers! --- template: full-screen background-image: url(images/thatsponsors.jpg) class: no-footer ??? sponsors! --- layout: true template: level-1 name: bio # Steven Hicks --- .profile.bio[   ] ??? I am standing right in front of you so you can tell what I look like But on the left is what my 7 year old daughter Olivia thinks I look like On the right is what my 10 year old daughter Lila thinks I look like and if you think this is just an excuse to talk about my kids and show you their artwork You are correct. --- ## JavaScript Engineer ??? for a company in MKE called NM --- layout: false template: full-screen class: inverse background-image: url(images/nm-view.jpg)
??? view from 10 ft from my desk big-company perks small-company tech ... one thing you should know about me: --- template: bio ## I really really enjoy writing tests ??? When I'm in a standup, and someone mentions they still need to write tests, I'll often volunteer for it --- layout: true template: full-screen class: square --- background-image: url(images/you.jpg) class: no-footer ??? so that's me but I want to know about you. * react developers? * little testing experience? * familiar with testing but not written react tests? * writing react tests? --- template: full-screen class: testing-pyramid, no-footer background-image: url(images/testing-pyramid-1.jpg) ??? Testing pyramid. If you haven't seen it before.... suggests types of tests (unit, integration,...) the shape suggests a balance - there's a tradeoff --- template: full-screen class: testing-pyramid, no-footer background-image: url(images/testing-pyramid-2.jpg) ??? We're going to focus on this area Not to say that the others aren't important! But the tooling & strategies are going to be different --- layout: false template: our-journey class: stage-0 ??? This is what our journey looks like today Arm you with tools Show you how to use those tools Tell you when to use a hammer, when to use a wrench ... Price is right? --- layout: false template: our-journey class: stage-1 journey-stage: 1 name: journey-1 --- template: level-2 layout: true name: getting-started # Getting Started --- template: title # Jest ## http://facebook.github.io/jest ??? The test framework I'm interested in showing today is Jest Can be used for any JS testing, not just React --- ## Why Jest? ??? several reasons --- layout: true template: getting-started ## Why Jest? --- ### Easy to set up ??? If you're using create-react-app, it's already set up! If not, it's probably less than three steps to set up. --- ### Zero configuration ??? If you **follow the conventions** for naming your test files, Jest will just find them. --- ### Interactive watch mode ??? By default, runs **only tests affected by changes** since your most recent commit. So as I'm making changes, the only tests running are the ones I'm working on. (Great for TDD) Other options: 1. run all tests 2. filter tests by name or file path --- ### Helpful error messages ??? This is one of my favorite things happening in dev right now Error messages that tell you **what you did wrong** instead of just **you did something wrong** ... These help you **pinpoint** what's wrong with your test -- .content-image.friendly-error[  ] --- ### Snapshot testing ??? Allows you to compare **what a component looks like** now to **what it looked like before** you made changes We'll look more later. But this is a huge feature for a lot of people doing React dev. --- layout: false template: title # Installing Jest ??? If you're using create-react-app, it's already set up! --- layout: true template: getting-started --- ## Installing Jest ### `npm install --save-dev jest` ??? For the most part...it's this simple. There might be a couple extra steps depending on your setup, -- .footnote[ ### http://facebook.github.io/jest ] ??? but the docs are thorough and helpful. --- class: list ## Jest Conventions -- ### \_\_tests\_\_ folders -- ### *.spec.js -- ### *.test.js --- template: title # Jest API --- template: getting-started layout: true name: jest-api-2 ## Jest API --- ### describe() ??? describe is how you define a **set of tests** --- template: level-3 layout: true name: jest-api-3 class: code # Getting Started ## Jest API --- ### describe() ``` describe('getLocalBeers', () => { // tests for getLocalBeers go here }); ``` .footnote[ get-local-beers.spec.js ] ??? It's kind of like **namespacing** your tests. --- ### describe() ``` describe('getLocalBeers', () => { describe('when there are lots of beers', () => { // tests for getLocalBeers/lots of beers go here }); }); ``` .footnote[ get-local-beers.spec.js ] ??? You can nest describes Great for scenarios that need multiple tests --- template: jest-api-2 ### it() or test() ??? use it or test to define an individual test. --- template: jest-api-3 ### it() or test() .dim-1.dim-7[ ``` describe('getLocalBeers', () => { it('calls the api', () => { // your test goes here }); }); ``` ] .footnote[ get-local-beers.spec.js ] ??? give it a name and a function to execute. --- template: jest-api-2 ### Assertions ??? and then you'll want to make assertions. --- template: jest-api-3 ### Assertions ```javascript expect(result.location).toEqual('Amherst, WI'); expect(result.name).not.toEqual('High Life'); expect(result.beers).toHaveLength(3); expect(result.beers).toEqual( expect.arrayContaining([{ name: 'Mudpuppy Porter' }]) ); ``` ??? expect() matchers (read em) they aren't much different than other JS test frameworks and if you're new to JS testing, they read very fluently. --- template: getting-started layout: false class: title-footnote ## Converting To Jest ### Jest Codemods .footnote[[github.com/skovhus/jest-codemods](https://github.com/skovhus/jest-codemods)] ??? If you're convinced... Jest Codemods will help you convert It's not trivial - there might still be things to do But this will help you get started --- layout: false template: our-journey class: stage-2 journey-stage: 2 name: journey-2 ??? How do we test our react app? --- template: level-1 # Testing React ## Business Logic ??? One of the most important things to test Also one of the easiest --- template: level-2 class: code, no-footer # Testing React ## Business Logic ```javascript it('filters out beers that aren`t porters', () => { const beers = [ { brewery: 'Central Waters', name: 'Mudpuppy Porter', beerStyle: 'Porter' }, { brewery: 'New Glarus', name: 'Spotted Cow', beerStyle: 'Farmhouse' } ]; const result = filterBeers(beers); expect(result).toHaveLength(1); expect(result[0].name).toEqual('Mudpuppy Porter'); }); ``` --- template: level-1 # Testing React ## Components ??? And this moment is probably what you're **most looking forward to** in this talk ... --- template: level-2 class: code, hide-footer # Testing React ## Components ### Are you sure it can't be extracted? ??? Except I'm going to make you **wait** even longer. Because I want you to ask yourself - does this logic I'm about to test need to live in this component? Or can I **extract it to a function**, and test that function in isolation? --- template: level-2 class: code # Testing React ## Extracted Logic ```javascript it('maps api beers to ui beers', () => { const apiBeer = { beerName: 'Mudpuppy Porter', beerId: '123456' }; const expectedUiBeer = { name: 'Mudpuppy Porter', id: '123456' }; const result = mapApiBeerToUiBeer(apiBeer); expect(result).toEqual(expectedUiBeer); }); ``` .footnote[ map-beers.spec.js ] ??? Especially useful with **mapping functions** reduce filter changing shape --- template: level-2 class: list # Testing React ## Components ??? And all of the things we want to test can be **categorized** as one of **two** things. ... -- ### Render ??? Does it render correctly? ... -- ### Interactions ??? Does it interact properly? "when I click this button, this div shows up" ... and for both those things, we'll want to pull in one more tool. --- template: level-2 # Testing React ## Components ### Enzyme --- template: level-3 # Testing React ## Components ### Enzyme > Enzyme is a Javascript testing utility for React that makes it easier to assert, manipulate, and traverse your React Components' output .footnote[ http://airbnb.io/enzyme/ ] ??? allows you to render your component into memory execute tests against it --- template: level-3 class: list # Testing React ## Components ### Why Enzyme? -- #### Intuitive API ??? Similar to jQuery's api for finding elements & interacting ... -- #### Shallow or deep rendering ??? It offers a variety of options for rendering - Which means you can test components how you want to test them - in full isolation, or in full integration. --- template: level-3 class: list # Testing React ## Components ### Installing Enzyme #### http://airbnb.io/enzyme/docs/installation/ ??? To install Enzyme....head to the docs. There are a couple of important details - installing an adapter that you don't want to miss --- template: title # Testing Component Render ??? To demonstrate how you'd test component render, I've got a scenario of components for you. --- template: level-3 layout: true name: testing-render class: code # Testing React ## Component Render --- template: testing-render layout: true name: our-scenario ### Our Scenario --- .content-image[  ] --- .content-image[  ] --- .content-image[  ] --- .content-image[  ] ??? render, but to the real actual DOM as close to the real browser as you'll get --- ```javascript import Brewery from './Brewery'; import BeerStyle from './BeerStyle'; export default function Beer({ beer }) { return (
{beer.name}
); } ``` .footnote[ Beer.jsx ] ??? Note: className="beer" --- ```javascript export default function Brewery({ brewery }) { return (
{brewery}
); } ``` .footnote[ Brewery.jsx ] ??? Note: className="brewery" --- template: testing-render layout: true ### shallow() --- -- ```javascript import { shallow } from 'enzyme'; it('renders a beer', () => { const beer = { name: 'Mudpuppy Porter' }; const wrapper = shallow(
); expect(wrapper.find('.beer')).toHaveLength(1); expect( wrapper.contains(
Mudpuppy Porter
) ).toBe(true); }); ``` .footnote[ Beer.spec.jsx ] --- ```javascript it('doesn`t shallow render brewery', () => { const beer = { name: 'Mudpuppy Porter', brewery: 'Central Waters' }; const wrapper = shallow(
); expect(wrapper.find('.brewery')).toHaveLength(0); expect( wrapper.contains(
Central Waters
) ).toBe(false); }); ``` .footnote[ Beer.spec.jsx ] ??? on the other hand... I wouldn't write a test like this for my code But this illustrates what shallow does. --- template: testing-render layout: false ### render() ??? renders the whole tree "integration testing" components -- ```javascript import { render } from 'enzyme'; it('renders brewery', () => { const beer = { name: 'Mudpuppy Porter', brewery: 'Central Waters' }; const wrapper = render(
); expect(wrapper.find('.brewery')).toHaveLength(1); }); ``` .footnote[ Beer.spec.jsx ] ??? Note we don't have access to wrapper.contains It uses a library called Cheerio provides some traversal, but it's not as nice to navigate. --- template: testing-render layout: false ### mount() ??? almost identical -- ```javascript import { mount } from 'enzyme'; it('mounts brewery', () => { const beer = { name: 'Mudpuppy Porter', brewery: 'Central Waters' }; const wrapper = mount(
); expect(wrapper.find('.brewery')).toHaveLength(1); expect( wrapper.contains(
Central Waters
) ).toBe(true); }); ``` .footnote[ Beer.spec.jsx ] ??? deep renders but it renders into the full DOM - so it's a better representation of what will actually happen AND I get the ability to do .contains --- template: title # Snapshot Tests ??? There's **another method** to testing render. Comes from **Jest** Takes a snapshot of the rendered component --- layout: true template: testing-render ### Snapshot Tests --- .content-image.snapshots[  ] ??? The first time you run the test, it'll put the rendered contents of your component in a file. Every time after, it'll **regenerate** the snapshot and **compare** it to the one in the file --- .content-image.snapshots[  ] ??? If it's the same, the test passes. --- .content-image.snapshots[  ] ??? If it's different, the test fails. It might have failed for good reason In which case you go fix the issue But it might just be a harmless change. --- .content-image.snapshots[  ] ??? In this case, you can tell Jest to **update the snapshot.** And you're back to a passing test. Jest relies on you to say **"yes, it's different, but it's still good."** --- class: full-width, no-footer  ??? * Error diff * Option to update --- class: code ```javascript it('snapshots a beer with shallow', () => { const beer = { name: 'Mudpuppy Porter', brewery: 'Central Waters' }; const wrapper = shallow(
); expect(wrapper).toMatchSnapshot(); }); ``` .footnote[ Beer.spec.jsx ] ??? Here's an example of a snapshot test ... and this will generate a snapshot that looks like this... --- class: code ```html
Mudpuppy Porter
``` .footnote[ Beer.spec.jsx.snap ] ??? of particular note is the
component - a placeholder, really, for a child component. ... if I use render, instead of shallow, the snapshot will look like this --- class: code .dim-1.dim-2.dim-3.dim-4.dim-5.dim-6.dim-7.dim-13[ ```html
Mudpuppy Porter
Central Waters
``` ] .footnote[ Beer.spec.jsx.snap ] ??? you can see that now I have the rendered markup from the brewery, not just a component placeholder. ... and if I use mount... --- class: code, no-footer .dim-1.dim-2.dim-3.dim-4.dim-5.dim-6.dim-7.dim-8.dim-9.dim-10.dim-20.dim-21.dim-22[ ```html
Mudpuppy Porter
Central Waters
``` ] --- layout: false template: testing-render class: list ### Pros of Snapshot Tests -- #### Simple to write ??? render component then expect something **toMatchSnapshot()**. ... -- #### Simple to fix ??? When one breaks, you just hit 'u' and it fixes the test maybe TOO easy... ... -- #### Speeds up development ??? I waver on this, because there are cons. --- layout: true template: testing-render class: list ### Cons of Snapshot Tests --- #### Break easily ??? They break really easily. Literally anything in your component changes and you've got a broken test. ... but the biggest downside... -- #### Optimized for **writing** tests over **fixing** tests ??? Snapshot tests make it easy to WRITE tests But **they make it harder to fix broken tests**. --- #### Who do we write tests for? ??? Consider. -- .content-image.context[  ] ??? Is it you, right now? In some sense, if you're doing TDD, yes. But they're just as useful, if not more, for... --- #### Who do we write tests for? .content-image.context[  ] ??? Your coworker with the sweet moustache 3 mos from now No context ... Snapshot tests are really cool - but I think they **make fixing tests harder**, because they **require** you to have **more knowledge** about the component being rendered To know if something is breaking or not. --- template: title # Html-looks-like .footnote[ [github.com/staltz/html-looks-like](https://github.com/staltz/html-looks-like) ] ??? A third method for testing render Uses a library called html-looks-like ... Html looks like allows you to specify a template that your markup should look like --- layout: true template: testing-render ### Html-looks-like --- class: code ```javascript it('looks like a beer', () => { const beer = { name: 'Mudpuppy Porter', brewery: 'Central Waters', }; const wrapper = render(
); expect(wrapper).toLookLike(`
Mudpuppy Porter
{{ brewery placeholder }}
`); }); ``` .footnote[ Beer.spec.jsx ] ??? And we can write a custom Jest matcher that uses html-looks-like to allows to expect something toLookLike a string of markup. ... You can't tell by these slides, because the syntax highlighting is too good - but that's actually a string that I'm comparing it to Which kind of stinks, and can be hard to maintain. --- class: code ```javascript it('looks like a beer', () => { const beer = { name: 'Mudpuppy Porter', brewery: 'Central Waters', }; const wrapper = render(
); expect(wrapper).toLookLike(
Mudpuppy Porter
); }); ``` .footnote[ Beer.spec.jsx ] ??? But it doesn't take much to write a toLookLike matcher that takes JSX and a Placeholder component where we don't care And now this test is **easier to read & maintain**. --- layout: false template: testing-render class: list ### Pros of Html-looks-like -- #### Clear tests ??? The only thing in our assertion is the markup we care about for the sake of the test --- layout: true template: testing-render class: list ### Cons of Html-looks-like --- #### It's very heavy ??? And the way you extend Jest to use a new matcher, your extension is added at the beginning of every single test. So it slows down your tests, noticeably. --- template: level-3 # Testing React ## Component Render ### Guidance #### Find the right balance ??? But the key - Find the balance of shallow vs deep vs snapshots vs html-looks-like Try them all. Me: move as much out of my components as possible Run simple tests of components, usually shallow rendered, to make sure things are shown/hidden correctly Using Enzyme if the assertion is simple to write Html-looks-like if it is not --- template: title # Testing Component Interactions ??? The key to testing interactions is ... --- template: level-3 layout: true name: testing-interactions-3 class: code # Testing React ## Component Interactions --- ### .simulate() ??? Enzyme's simulate function. -- ```javascript it('simulates a click event', () => { //arrange const wrapper = shallow(...); //act wrapper.find('button').simulate('click', parameters); //assert // ... }); ``` ??? .simulate lets you simulate an event on an element. So it's easy to test things like **"when the user clicks the button, this other element gets rendered."** --- layout: true template: testing-interactions-3 ### Our Scenario --- -- ```html
This is the content that shows up when expanded.
``` ??? Example of usage --- ```javascript export default class CollapsePanel extends React.Component { render() { return (
{this.props.title}
{this.renderChildren()}
); } //... } ``` .footnote[ CollapsePanel.jsx ] ??? The component might look like this --- .dim-1.dim-2.dim-15.dim-16.dim-17[ ```javascript export default class CollapsePanel extends React.Component { render() { ... } renderChildren() { if (this.state.isCollapsed) { return null; } return (
{this.props.children}
); } //... } ``` ] .footnote[ CollapsePanel.jsx ] ??? The renderChildren function looks like this --- .dim-1.dim-2.dim-3.dim-4.dim-5.dim-15[ ```javascript export default class CollapsePanel extends React.Component { render() { ... } renderChildren() { ... } state = { isCollapsed: true }; toggleCollapsed = () => { this.setState(prevState => { return { isCollapsed: !prevState.isCollapsed }; }); }; } ``` ] .footnote[ CollapsePanel.jsx ] ??? And state mgmt like this --- template: level-2 layout: true name: testing-interactions-2 class: code # Testing React ## Component Interactions --- ```javascript it('is collapsed by default', () => { const wrapper = shallow(
Some contents here
); expect(wrapper.find('.collapse-panel')).toHaveLength(1); expect(wrapper.find('.collapse-panel-content')).toHaveLength(0); }); ``` .footnote[ CollapsePanel.spec.jsx ] ??? We might write one test to **verify the default state** --- ```javascript it('uncollapses after clicking', () => { const wrapper = shallow(
Some contents here
); wrapper.find('.collapse-panel-toggle').simulate('click'); expect(wrapper.find('.collapse-panel-content')).toHaveLength(1); }); ``` .footnote[ CollapsePanel.spec.jsx ] ??? And another test to **verify the expanded state.** --- template: title # Testing Component State ??? Now you might think there is a third thing in your react app you want to test In this case, I'm really talking about components that are using setState to store component-level state. --- template: level-2 layout: true name: testing-state class: code # Testing React ## Component State --- -- ### Usually covered by interaction tests ??? Think back to our collapsible panel example We don't care so much about the fact that it's setting state What we care about is that when we click this button on a collapsible panel, it expands the content. And we can accomplish that test by .simulating a click And then verifying that an element appears. --- ### Testing state is testing implementation ??? You can do it...but I advise against it. --- layout: false template: our-journey class: stage-3 journey-stage: 3 name: journey-3 ??? We've got tools Know how to use them Good practices? --- template: level-1 # Guidance ## Better Components ??? It starts with writing better components. We can write them in a way that makes writing tests easier. --- template: level-2 layout: true # Guidance ## Better Components --- ### Extract Complicated Things & Business Logic ??? Test them in isolation! This leaves your components mostly dumb And way easier to test --- ### Decompose Components By Responsibility ??? Small components do fewer things And are therefore easier to test but you don't want to make them so small that they don't have meaning break up by responsibility allows you to shallow render components for tests, instead of deep list+list item example --- template: level-1 # Guidance ## Better Tests ??? With better components, we can focus on writing better tests. --- template: level-2 layout: true name: better-tests # Guidance ## Better Tests --- ### Embrace describe() ??? Describe **functions** under test And **scenarios** that require multiple tests within them. Keep tests **organized** & easy to find things Don't go more than 2 or 3 describes deep or it means your function is doing too much ... --- template: full-screen background-image: url(images/junk-drawer.jpg) class: inverse ??? But don't be **monsters**, and write all of your tests outside of describe blocks. (story about just adding tests wherever) --- ### Minimize Setup ??? When you have common setup, extract it into composable functions Tests are easier to read when we can focus on what makes them unique. Signal:noise --- template: level-3 layout: true # Guidance ## Better Tests ### Minimize Setup --- #### Chekhov's Gun --- >Remove everything that has no relevance to the story. If you say in the first chapter that there is a rifle hanging on the wall, in the second or third chapter it absolutely must go off. If it's not going to be fired, it shouldn't be hanging there. .footnote[ Anton Chekhov ] ??? same rule applies to setup. If the test isn't using it...don't set it up. allows reader to focus on *what makes this test unique* --- class: no-footer ```javascript describe('validateBeer', () => { it('returns invalid for beers with no abv', () => { const beer = { id: 87983, brewery: { id: 12332, name: 'Central Waters', location: 'Amherst, WI', overallRating: 5 }, name: 'Mudpuppy Porter', beerStyle: 'Porter', abv: undefined }, const result = validateBeer(beer); expect(result).toEqual(false); }); }) ``` .footnote[ validate-beer.spec.js ] --- class: no-footer .dim-1.dim-2.dim-3.dim-4.dim-5.dim-6.dim-7.dim-8.dim-9.dim-10.dim-11.dim-12.dim-14.dim-15.dim-16.dim-17.dim-18.dim-19.dim-20[ ```javascript describe('validateBeer', () => { it('returns invalid for beers with no abv', () => { const beer = { id: 87983, brewery: { id: 12332, name: 'Central Waters', location: 'Amherst, WI', overallRating: 5 }, name: 'Mudpuppy Porter', beerStyle: 'Porter', abv: undefined // <--------- }, const result = validateBeer(beer); expect(result).toEqual(false); }); }) ``` ] .footnote[ validate-beer.spec.js ] --- ```javascript describe("validateBeer", () => { it("returns invalid for beers with no abv (less setup)", () => { const beer = makeMeABeer(); beer.abv = undefined; const result = validateBeer(beer); expect(result).toEqual(false); }); }); ``` .footnote[ validate-beer.spec.js ] ??? makeMeABeer would be another function in this test file that just makes a beer with sensible defaults And then you can override what you want --- template: better-tests layout: true --- ### Keep Snapshots Small ??? Snapshotting big things is cumbersome hard to track what changed ... and along that note... --- ### Use [enzyme-to-json ](https://github.com/adriantoine/enzyme-to-json) --- template: level-3 layout: false # Guidance ## Better Tests ### enzyme-to-json ```html
Mudpuppy Porter
``` ??? otherwise you'll get noise from the enzyme wrapper --- template: better-tests ### Extend Jest with .toLookLike() sparingly --- template: better-tests ### Consider Your Audience .content-image.context[  ] --- template: title # Optimize for fixing tests ## instead of writing tests ??? Write in a way that when they fail in several months, you can tell immediately why the failed and how to fix them. --- layout: false template: cover-art # THANK YOU! .about.right[ ## Steven Hicks ### [@pepopowitz](http://twitter.com/pepopowitz)
### steven.j.hicks@gmail.com
### [stevenhicks.me/react-testing](https://stevenhicks.me/react-testing)
] ??? I want to thank you for your time I really appreciate it. Survey Questions after ... --- template: full-screen background-image: url(images/that2019.jpg) ??? I'll be here next year. I hope you will too! Thank you! --- template: level-1 class: double-wide, resources # Resources * [This talk](https://stevenhicks.me/react-testing) * [Code Samples](https://github.com/pepopowitz/unit-testing-your-react-app) * [Jest](https://facebook.github.io/jest/) * [Enzyme](http://airbnb.io/enzyme) * [enzyme-to-json](https://github.com/adriantoine/enzyme-to-json) * [Additional matchers (assertions)](https://github.com/blainekasten/enzyme-matchers) * [Pain points of jest/enzyme](https://medium.com/@Tetheta/lessons-learned-testing-react-redux-apps-with-jest-and-enzyme-eb581d6d167b) --- template: level-1 class: double-wide, resources # Images * [Loic Djim](https://unsplash.com/photos/Sq7WPOjHGDs) - Title * [mgstanton](https://www.flickr.com/photos/marirn/6131270109/) - Junk Drawer * The rest - Me