Skip to content

Commit 99c6831

Browse files
committed
The example and docs for /py/facets are almost done.
1 parent 41ed4e0 commit 99c6831

File tree

2 files changed

+27
-41
lines changed

2 files changed

+27
-41
lines changed

python/example-pytest-selfie/tests/selfie_settings.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,15 @@ def _html_to_md(html: str):
5252
return trim_lines.strip()
5353

5454

55-
HTML_LENS = (
55+
_HTML_LENS = (
5656
CompoundLens()
5757
.mutate_facet("", _pretty_print_html)
5858
.replace_all_regex("http://localhost:\\d+/", "https://demo.selfie.dev/")
5959
.set_facet_from("md", "", _html_to_md)
6060
)
6161

62-
WEB_CAMERA = Camera.of(_web_camera).with_lens(HTML_LENS)
62+
_WEB_CAMERA = Camera.of(_web_camera).with_lens(_HTML_LENS)
6363

6464

6565
def web_selfie(response: TestResponse) -> StringSelfie:
66-
return expect_selfie(response, WEB_CAMERA)
66+
return expect_selfie(response, _WEB_CAMERA)

selfie.dev/src/pages/py/facets.mdx

Lines changed: 24 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ You can write `web_selfie` anywhere, but we recommend putting it into `selfie_se
6767

6868
## Facets
6969

70-
Every snapshot has a "subject": `Snapshot.of(subject: str)`. But each snapshot can also have an unlimited number of "facets", which are other named values. For example, maybe we want to add the response's status code.
70+
Every snapshot has a **subject**: `Snapshot.of(subject: str)`. But each snapshot can also have an unlimited number of **facets**, which are other named values. For example, maybe we want to add the response's status code.
7171

7272
```python
7373
def web_selfie(response: TestResponse) -> StringSelfie:
@@ -116,7 +116,7 @@ So a snapshot doesn't have to be only one value, and it's fine if the schema cha
116116

117117
## Cameras
118118

119-
So if you want to capture multiple facets of something, you need a function which turns that something into a `Snapshot`. Selfie calls this idea a `Camera`. You can pass a `Camera` as the second argument to `expect_selfie`, which would look like so:
119+
If you want to capture multiple facets of something, you need a function which turns that something into a `Snapshot`. Selfie calls this a `Camera`. You can pass a `Camera` as the second argument to `expect_selfie`, which would look like so:
120120

121121
```python
122122
def _web_camera(response: TestResponse) -> Snapshot:
@@ -149,10 +149,13 @@ One option is to call this function inside the `Camera`. But this mixes concerns
149149
```python
150150
def _web_camera(response: TestResponse) -> Snapshot: ...
151151
def _pretty_print_html(html : str) -> str: ...
152-
def _pretty_print_lens(snapshot : Snapshot) -> Snapshot:
153-
if (snapshot.subject.contains("<html"))
154-
return snapshot.with_subject(_pretty_print_html(snapshot.subject))
155-
else
152+
def _pretty_print_lens(snapshot: Snapshot) -> Snapshot:
153+
if "<html" in snapshot.subject.value_string():
154+
# You can think of a `Snapshot` is an immutable dict of facets
155+
# The value of each facet is either `str` or `bytes`
156+
# The "subject" is a special facet whose key is ""
157+
return snapshot.plus_or_replace("", _pretty_print_html(snapshot.subject.value_string()))
158+
else:
156159
return snapshot
157160

158161
_WEB_CAMERA = Camera.of(_web_camera).with_lens(_pretty_print_lens)
@@ -161,48 +164,39 @@ def web_selfie(response: TestResponse) -> StringSelfie:
161164
return expect_selfie(response, _WEB_CAMERA)
162165
```
163166

164-
Calling transformation functions inside the `Camera` is fine, but another option is to create a `Lens` and then use `Camera.withLens`. This approach is especially helpful if there are multiple `Camera`s which need the same transformation.
165-
167+
By keeping the lens separate from the camera, you can also reuse the lens in other cameras. For example, you might want to pretty-print the HTML in an email.
166168

167169
## Compound lens
168170

169-
Selfie has a useful class called [`CompoundLens`](https://github.com/diffplug/selfie/issues/324). It is a fluent API for mutating facets and piping data through functions from one facet into another. An important gotcha here is that the **subject** can be treated as a facet named `""` (empty string). `CompoundLens` uses this hack to simplify a snapshot into only a map of facets, instead of a subject plus a map of facets.
171+
The example above has some nasty plumbing for dealing with the `Snapshot` API. To make this easier, you can use `CompoundLens`. It is a fluent API for mutating facets and piping data through functions from one facet into another. An important gotcha here is that the **subject** can be treated as a facet named `""` (empty string). `CompoundLens` uses this hack to simplify a snapshot into only a map of facets, instead of a subject plus a map of facets.
170172

171173
We can easily mutate a specific facet, such as to pretty-print HTML in the subject...
172174

173-
**TODO: [Not implemented](https://github.com/diffplug/selfie/issues/324)**
174-
175175
```python
176-
HTML = CompoundLens().mutate_facet("", lambda maybe_html: pretty_print_html(maybe_html) if "<html>" in maybe_html else None)
176+
_HTML_LENS = CompoundLens().mutate_facet("", _pretty_print_html)
177177
```
178178

179-
Or we can mutate all facets, such as to remove a random local port number...
180-
181-
**TODO: [Not implemented](https://github.com/diffplug/selfie/issues/324)**
179+
Or we can mutate every facet, such as to remove a random local port number...
182180

183181
```python
184-
HTML = (
185-
CompoundLens()
186-
.mutate_facet("", lambda maybe_html: pretty_print_html(maybe_html) if "<html>" in maybe_html else None)
182+
_HTML_LENS = CompoundLens() \
183+
.mutate_facet("", _pretty_print_html) \
187184
.replace_all_regex("http://localhost:\\d+/", "https://www.example.com/")
188-
)
189185
```
190186

191187
Or we can render HTML into markdown, and store the easy-to-read markdown in its own facet...
192188

193-
**TODO: [Not implemented](https://github.com/diffplug/selfie/issues/324) yet, use markdown2?**
194-
195189
```python
196-
import markdown2
190+
from markdownify import markdownify as md
197191

198-
def html_to_md(html):
199-
return markdown2.markdown(html)
192+
def _html_to_md(html: str) -> str:
193+
return md(html) if "<html" in html else None
200194

201195
HTML = (
202196
CompoundLens()
203-
.mutate_facet("", SelfieSettings.pretty_print_html)
197+
.mutate_facet("", _pretty_print_html)
204198
.replace_all_regex("http://localhost:\\d+/", "https://www.diffplug.com/")
205-
.set_facet_from("md", "", html_to_md)
199+
.set_facet_from("md", "", _html_to_md)
206200
)
207201
```
208202

@@ -211,28 +205,20 @@ HTML = (
211205
Snapshot testing has been badly underused for three reasons:
212206

213207
- controlling read vs write used to be cumbersome (fixed by [control comments](/py/get-started#quickstart))
214-
215-
**TODO: Selfie garbage collection not implemented [yet](https://github.com/diffplug/selfie/issues/325), PRs welcomed!**
216-
217-
- stale snapshots used to pile up (fixed by [garbage collection](https://github.com/diffplug/selfie/issues/325))
208+
- stale snapshots used to pile up (fixed by garbage collection [TODO](https://github.com/diffplug/selfie/issues/325))
218209
- a great test should tell a story, and disk snapshots can't do that
219210

220211
Inline snapshots are a partial fix for storytelling within a test, but the harnessing can become verbose. This is where we combine it all:
221212

222213
- exhaustive specification on disk
223214
- succinct storytelling inline
224-
225-
**TODO: Selfie `Camera` and `CompoundLens` not implemented [yet](https://github.com/diffplug/selfie/issues/302), PRs welcomed!**
226-
227-
- minimal boilerplate thanks to [`Camera`](#typed-snapshots) and [`CompoundLens`](#compound-lens)
215+
- minimal boilerplate thanks to [`Camera`](#cameras) and [`CompoundLens`](#compound-lens)
228216

229217
Let's look at a test that puts all of this together.
230218

231-
**TODO: Not implemented [yet](https://github.com/diffplug/selfie/issues/303), PRs welcomed!**
232-
233219
```python
234220
def test_login_flow(app):
235-
expect_selfie(get("/")).to_match_disk("1. not logged in").facet("md").to_be("Please login")
221+
web_selfie(get("/")).to_match_disk("1. not logged in").facet("md").to_be("Please login")
236222

237223
expect_selfie(given().param("email", "[email protected]").post("/login")).to_match_disk("2. post login form")\
238224
.facet("md").to_be("""Email sent!
@@ -257,7 +243,7 @@ [email protected]|JclThw==;Path=/""")
257243
status code: 401""")
258244
```
259245

260-
We just wrote a high-level specification of a realistic login flow, and it only took 25 lines of java code — most of which were generated for us, and could be regenerated on a whim if we want to change our copywriting. The [corresponding disk snapshot](https://github.com/diffplug/selfie/issues/322) gives us an exhaustive specification and description of the server's behavior.
246+
We just wrote a high-level specification of a realistic login flow, and it only took TODO lines of python code — most of which were generated for us, and could be regenerated on a whim if we want to change our copywriting. The [corresponding disk snapshot TODO](https://github.com/diffplug/selfie/issues/322) gives us an exhaustive specification and description of the server's behavior.
261247

262248
Didn't think that adopting a bugfixed version of your internationalization lib would cause any changes to your website whatsever? Oops. Don't wade through failed assertions, get a diff in every failure. If you want, regenerate all the snapshots to get a full view of the problem across the whole codebase in your git client.
263249

0 commit comments

Comments
 (0)