|
| 1 | +import { FooterCTA } from "@/components/FooterCTA/FooterCTA"; |
| 2 | +import { NavHeading } from "@/components/NavHeading"; |
| 3 | + |
| 4 | +export const showHeroLinks = 'true'; |
| 5 | +export const title = "Selfie Python Snapshot Testing"; |
| 6 | +export const description = "Zero-config inline and disk snapshots for Python. Features garbage collection, filesystem-like APIs for snapshot data, and novel techniques for storytelling within test code."; |
| 7 | + |
| 8 | +<NavHeading text="literal" popout="/py/get-started#quickstart" /> |
| 9 | + |
| 10 | +## NOT READY YET - WIP |
| 11 | + |
| 12 | +This is a reasonable way to test. |
| 13 | + |
| 14 | +```python |
| 15 | +@Test |
| 16 | +public void primesBelow100() { |
| 17 | + Assertions.assertThat(primesBelow(100)).startsWith(2, 3, 5, 7).endsWith(89, 97); |
| 18 | +} |
| 19 | +``` |
| 20 | + |
| 21 | +But oftentimes a more useful way to test is actually: |
| 22 | + |
| 23 | +```python |
| 24 | +@Test |
| 25 | +public void testMcTestFace() { |
| 26 | + System.out.println(primesBelow(100)); |
| 27 | +} |
| 28 | +``` |
| 29 | + |
| 30 | +With literal snapshots, you can `println` directly into your testcode, combining the speed and freedom of `println` with the repeatability and collaborative spirit of conventional assertions. |
| 31 | + |
| 32 | +```python |
| 33 | +@Test |
| 34 | +public void primesBelow100() { |
| 35 | + expectSelfie(primesBelow(100).toString()).toBe_TODO(); |
| 36 | +} |
| 37 | +``` |
| 38 | + |
| 39 | +When you run the test, selfie will automatically rewrite `_TODO()` into whatever it turned out to be. |
| 40 | + |
| 41 | +```python |
| 42 | +@Test |
| 43 | +public void primesBelow100() { |
| 44 | + expectSelfie(primesBelow(100).toString()) |
| 45 | + .toBe("[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]"); |
| 46 | +} |
| 47 | +``` |
| 48 | + |
| 49 | +And from now on it's a proper assertion, but you didn't have to spend any time writing it. It's not only less work, but also more complete than the usual `.startsWith().endsWith()` rigamarole. |
| 50 | + |
| 51 | +<NavHeading text="like-a-filesystem" popout="/py/get-started#disk" /> |
| 52 | + |
| 53 | +## NOT READY YET - WIP |
| 54 | + |
| 55 | +That `primesBelow(100)` snapshot above is almost too long. Something bigger, such as `primesBelow(10_000)` is definitely too big. To handle this, selfie lets you put your snapshots on disk. |
| 56 | + |
| 57 | +```python |
| 58 | +@Test |
| 59 | +public void gzipFavicon() { |
| 60 | + expectSelfie(get("/favicon.ico", ContentEncoding.GZIP)).toMatchDisk(); |
| 61 | +} |
| 62 | + |
| 63 | +@Test |
| 64 | +public void orderFlow() { |
| 65 | + expectSelfie(get("/orders")).toMatchDisk("initial"); |
| 66 | + postOrder(); |
| 67 | + expectSelfie(get("/orders")).toMatchDisk("ordered"); |
| 68 | +} |
| 69 | +``` |
| 70 | + |
| 71 | +This will generate a snapshot file like so: |
| 72 | + |
| 73 | +```html |
| 74 | +╔═ gzipFavicon ═╗ base64 length 12 bytes |
| 75 | +Umlja1JvbGwuanBn |
| 76 | +╔═ orderFlow/initial ═╗ |
| 77 | +<html><body> |
| 78 | + <button>Submit order</button> |
| 79 | +</body></html> |
| 80 | +╔═ orderFlow/ordered ═╗ |
| 81 | +<html><body> |
| 82 | + <p>Thanks for your business!</p> |
| 83 | + <details> |
| 84 | + <summary>Order information</summary> |
| 85 | + <p>Tracking #ABC123</p> |
| 86 | + </details> |
| 87 | +</body></html> |
| 88 | +``` |
| 89 | + |
| 90 | +Selfie's snapshot files `.ss` are simple to parse, just split them up on `\n╔═`. Escaping rules only come into play if the content you are escaping has lines that start with `╔`, and you can always use `selfie-lib` as a parser if you want. |
| 91 | + |
| 92 | +You can treat your snapshot files as an output deliverable of your code, and use them as an input to other tooling. |
| 93 | + |
| 94 | +<NavHeading text="lensable" popout="/py/facets" /> |
| 95 | + |
| 96 | +## NOT READY YET - WIP |
| 97 | + |
| 98 | +A problem with the snapshots we've shown so far is that they are one dimensional. What about headers and cookies? What about the content the user actually sees, without all the markup? What if we could do this? |
| 99 | + |
| 100 | +``` |
| 101 | +╔═ orderFlow/initial [md] ═╗ |
| 102 | +Submit order |
| 103 | +╔═ orderFlow/ordered [md] ═╗ |
| 104 | +Thanks for your business!</p> |
| 105 | +``` |
| 106 | + |
| 107 | +Well, you can! Every snapshot has a *subject*, which is the main thing you are recording. And that subject can have any number of *facets*, which are named views of the subject from a different lens. |
| 108 | + |
| 109 | +```python |
| 110 | +var html = "<html>..." |
| 111 | +var snapshot = Snapshot.of(html).plusFacet("md", HtmlToMdParser.parse(html)) |
| 112 | +expectSelfie(snapshot).toMatchDisk() |
| 113 | +``` |
| 114 | + |
| 115 | +You can also use facets in combination with disk and inline literal snapshots to make your tests more like a story. |
| 116 | + |
| 117 | +```python |
| 118 | +@Test |
| 119 | +public void orderFlow() { |
| 120 | + expectSelfie(get("/orders")).toMatchDisk("initial") |
| 121 | + .facet("md").toBe("Submit order"); |
| 122 | + postOrder(); |
| 123 | + expectSelfie(get("/orders")).toMatchDisk("ordered") |
| 124 | + .facet("md").toBe("Thanks for your business!"); |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +Selfie's faceting is built around [Camera](https://kdoc.selfie.dev/selfie-lib/com.diffplug.selfie/-camera/), [Lens](https://kdoc.selfie.dev/selfie-lib/com.diffplug.selfie/-lens/), and [Snapshot](https://kdoc.selfie.dev/selfie-lib/com.diffplug.selfie/-snapshot/), whose API is roughly: |
| 129 | + |
| 130 | +```python |
| 131 | +final class Snapshot { |
| 132 | + final SnapshotValue subject; |
| 133 | + final ImmutableSortedMap<String, SnapshotValue> facets; |
| 134 | +} |
| 135 | +interface Lens { |
| 136 | + Snapshot transform(Snapshot snapshot); |
| 137 | +} |
| 138 | +interface Camera<T> { |
| 139 | + Snapshot snapshot(T subject); |
| 140 | + default Camera<T> withLens(Lens lens) { |
| 141 | + // returns a new Camera which applies the given lens to every snapshot |
| 142 | + } |
| 143 | +} |
| 144 | +``` |
| 145 | + |
| 146 | +See the [facets section](/py/facets) for more details on how you can use Selfie for snapshot testing with Java, Kotlin, or any JVM language. |
| 147 | + |
| 148 | +<NavHeading text="cacheable" popout="/py/cache" /> |
| 149 | + |
| 150 | +## NOT READY YET - WIP |
| 151 | + |
| 152 | +Sometimes a test has a component which is slow, expensive, or non-deterministic. In cases like this, it can be useful to save the result of a previous execution of the API call, and use that as a mock for future tests. |
| 153 | + |
| 154 | +```python |
| 155 | +var client = ExpensiveAiService(); |
| 156 | +var chatResponse = cacheSelfie(() -> { |
| 157 | + return client.chat("What's your favorite number today?"); |
| 158 | +}).toBe("Since it's March 14, my favorite number is π") |
| 159 | +// build other stuff with the chat response |
| 160 | +``` |
| 161 | + |
| 162 | +You can cache simple strings, but you can also cache typed API objects, binary data, or anything else you can serialize to a string or a byte array. |
| 163 | + |
| 164 | +```python |
| 165 | +var imageBytes = cacheSelfieBinary(() -> { |
| 166 | + return client.generateImage("A robot making a self portrait"); |
| 167 | +}).toBeFile("selfie.png") |
| 168 | +``` |
| 169 | + |
| 170 | +For more information on how to use `cacheSelfie`, see the [cache example](/py/cache). |
| 171 | + |
| 172 | +<FooterCTA /> |
0 commit comments