私が歌川です

@utgwkk が書いている

Pythonのテストで常にHTTPリクエストをモックする

HTTPリクエストをモックするには

PythonでHTTPリクエストをモックするには、どうしたらいいか。unittest.mockを使ってHTTPリクエストを投げるメソッドをモックする方法が思い付くかもしれない。しかし、どこをモックして何を返すようにしたらよいのかは自明ではない。

HTTPrettyというライブラリを使うと、簡単にHTTPリクエストをモックできる。どんなライブラリを使っているのか、インタフェースがどうなのかを気にする必要がない。

テスト中は常にモックする

PerlのTest::WWW::Stubに近いことをPythonでも実現したい。つまり、テスト中は常にHTTPリクエストをモックして外部にリクエストが飛ばないようにしたい。

pytestを使っているなら、以下のようなfixtureをconftest.pyに書くのが手軽だと思う。

import httpretty
import pytest


@pytest.fixture(scope="function", autouse=True)
def mock_http_request():
    with httpretty.enabled(allow_net_connect=False):
        yield

こうすると、外部にHTTPリクエストを飛ばしたら例外が送出されてテストが落ちるようになる。

Python標準のunittestでは試してないけど、TestCaseクラスを継承して、HTTPリクエストをモックするsetUpメソッド (片付けるtearDownメソッド) を実装すれば実現できるのではないか。

特定のリクエストに対してダミーレスポンスを返す

特定のURLへのリクエストにダミーのレスポンスを返したいなら、 yield の前で httpretty.register_uri 関数を呼び出すようにしたらいいし、テストメソッド内で呼ぶようにしてもよさそう。ダミーレスポンスを適当にファイルに切り出しておいたら取り回しがいいと思う。

以下の例では、Slack APIの呼び出しをモックしてダミーのレスポンスを返すように差し替えている。

from pathlib import Path
import httpretty
import pytest


@pytest.fixture(scope="function", autouse=True)
def mock_http_request():
    with httpretty.enabled(allow_net_connect=False):
        body = (
            Path(__file__).parent / "data/httpmock/slack_chat_postMessage.json"
        ).read_text()
        httpretty.register_uri(
            httpretty.POST, re.compile(r"^https://(www[.])?slack[.]com/api/chat[.]postMessage$"), body=body
        )
        yield

他の方法との比較

テストメソッドごとにHTTPリクエストをモックするかどうか決めたいならpytest-httprettyというプラグインが便利そう。httpretty というマーカーをテストに設定したらHTTPリクエストがモックされるようになる。

個人的には、常にHTTPリクエストがモックされている方が、テストの再現性や本番のAPIを叩いてしまうことを回避する側面などを考えると、よりよいと思ったので自分で仕組みを用意した。モックしたいリクエストだけモックするのではなく、常に外部リクエストが飛ばないようにして必要に応じて開放する方が安心できると思う。

Sentakuki Reliability Engineering

洗濯機のホース外れにはあまりにも無力。ただちに修理業者を呼んで直してもらった。びしょびしょになる範囲がそれほど大きくなかったこと、当日中に復旧できたことがよかったと思う。

Gitのコミットメッセージを複数行書くときは2行目を常に空行にするとよさそう

Git (GitHub) のコミットログに Co-authored-by を書くときに、2行目を空行にしないとGitHub上で共同編集したような表示にならない、ということがあった。2行目を空行にしてcommitし直したら出るようになった。

この挙動自体はドキュメントを読むと書いてありそう。

Tip: If you're using a text editor on the command line to type your commit message, ensure there are two newlines between the end of your commit description and the Co-authored-by: commit trailer.


慣習的に、コミットメッセージの2行目は空行にしているけど、2行目が空行であることを前提とした仕組みがいろいろあるっぽい。常に2行目は空行にするのがよさそう。いろいろ調べようとしたけど既にまとまっていたので、この記事ではリンクを貼って締めます。全体的にメールと地つづきな感じがある。

blog.unasuke.com

blog.ssanj.net

「野生の勘」を書き起すと

たびたび野生の勘について書いている気がする*1*2。ちょっとずつ、こういう感じでやっていますというのを言い表せるようになったのでちょっと書いてみます。

片っ端から型定義を見に行く

TypeScriptの場合、定義ジャンプを駆使するとライブラリの型定義ファイルに行き着くことがある。型定義があるということは、それが公開されているインタフェースの全てである、と考えることもできる。片っ端から見ていくと、思いがけず使えそうな道具を発見することもある。ドキュメントと照らし合わせたらだいたい答え合わせになる。

似たようなものとしては、ドキュメントを片っ端から読みあさるというのもある。規格でもいい。手札が多いと便利みたいなイメージ。確かこのあたりにあったような、という概念を少しでも増やしておくと助かることもある。試しになにか作ってみるのが手っ取り早そう。

ガチャガチャ試して記録する

シャッとやったら終わるようなタスク以外については、試行錯誤の跡を残しておくのがよいと考えている。思ったとおりうまくいきましたね、とか、こういう方針でいけると思ったけどここでうまくいかないようでした、みたいなのを記録しておく。あとから読み返すと、意外とおもしろいことをやっている、とか、実はこの作戦が違うところで使えるのでは?? とか、気づきがある。

このあと何が起こるかについて思いを馳せる

解決したい課題の全体像があって、そのうちの1タスクに対する解法だけ見ても、全体で見て整合性が取れているか、解決したい課題を解決できるかは明らかでない。この解法を取り込んだ後に何が起こるのか、について想像してみて、その上でどうなりそうか、どういう形が望ましいかを考えてみる。

大きなタスクにそのまま取り組むのではなくて、サブタスクに切り出してから着手しましょう、とよく言われる。流れを逆算しつつ適切な文脈でタスクを切るときも、何ができたらどうなって、どうなったら完成か、ちょっと先のことを考えることになる。

Webフォントを分割して読み込む際にunicode-rangeを指定しなかったらどうなるのか

表題のことについて検証してみましょう。

目次

前提

本題に入る前に、Webフォントを配信する際の前提について説明します。分かっている方は読み飛ばしてもらって大丈夫です。

Webページ上で独自のフォントを読み込むには

CSSの font-family 属性に使いたいフォント名を書くことで、Webページ上のコンテンツのフォントを変えることができます。

昔は、あらゆるシステムにインストールされていることが想定されているフォント*1を使うぐらいしかできませんでしたが、最近ではCSSに独自のフォントを読み込むための仕組みが用意されています。

より詳しくは ウェブフォント - ウェブ開発を学ぶ | MDN を読んでください。

フォントファイルのサイズ

一般に、フォントに含まれる文字数とフォントファイルのサイズは比例します。英数字・記号ぐらいならまだよいのですが、ラテン文字・キリル文字・ひらがな・カタカナ・漢字……と文字を含めていくとどんどん大きなフォントファイルになります。

参考までに、2022/3/24 時点のNoto Sansフォント (TrueType形式) は340KBですが、Noto Sans CJKのJPフォント (同じくTrueType形式) は34MBもあります*2

ページを閲覧したら34MBのファイルをダウンロードすることになる、と考えると迫力がありますね。実際にはフォントファイルを強くキャッシュすることができるはずですが、転送量はできるだけ抑えられると、ユーザー体験にも運用コスト面にも優しくなります。

フォントファイルのサイズを抑える工夫

何もフォントを読み込まない、ということになるとデザインに大きく制約がかかることになります。とはいえあまり大きなフォントファイルを配信するとユーザー体験が悪くなるので、なんとかしてファイルサイズを抑えることを考えます。

フォントファイルのサイズを抑える方針として、大きく以下の2つがあります。これらを組み合わせることで、転送量を抑えつつリッチなデザインのWebページを作ることができます。

  • フォントファイルを分割し、必要に応じて読み込む
  • フォントファイルを圧縮する

それぞれ簡単に説明します。

フォントファイルを分割し、必要に応じて読み込む

フォントファイルから必要なグリフ (文字) のデータだけを抽出する処理をサブセット化といいます。文字種ごとにサブセット化を繰り返すことで、フォントファイルを分割できます。サブセット化は、fontToolsのpyftsubsetコマンドや各種ソフトウェアによって実現できます。

分割したフォントファイルを必要に応じて読み込むには、CSSの @font-family ルールを使います。以下は、Google Fontsが配信しているUbuntuフォントにおける記述例です。

/* 前略 */
/* latin-ext */
@font-face {
  font-family: 'Ubuntu';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/ubuntu/v19/4iCs6KVjbNBYlgoKcQ72nU6AF7xm.woff2) format('woff2');
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Ubuntu';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/ubuntu/v19/4iCs6KVjbNBYlgoKfw72nU6AFw.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

Ubuntuという名前のfont-familyで、複数の @font-face ルールを記述しています。文字種ごとに読み込むべきフォントファイルを定義しています。このとき、どの文字種 (Unicodeのコードポイントの範囲) に対して有効なフォントなのかを宣言するために unicode-range 記述子を使っています。

フォントファイルを圧縮する

フォントファイルを分割することでファイルサイズを抑えることができますが、さらに圧縮をかけるともっと小さくすることができます。フォントファイルを圧縮する際のフォーマットとしては、WOFFやWOFF2があります。フォントファイルの圧縮もfontToolsのpyftsubsetコマンドで実現できます。

手元でNoto Sansフォント (TrueType形式) を圧縮すると、それぞれ以下のサイズになりました。WOFFではファイルサイズが半分近く、WOFF2ではそれ以下に削減されています。

フォント 圧縮前 WOFF WOFF2
Noto Sans Regular 340KB 177KB 120KB
Noto Sans CJK jp Regular 34MB 19MB 14MB

前提おわり

ここまで前提でした。リッチなユーザー体験とコスト削減を両立するために、Webフォントを配信する際に気をつける点がいくつかあることを分かっていただけたかと思います。

本題

ここから本題に入ります。Webフォントを読み込む際に unicode-range を指定しなかったらどうなるのでしょうか?

規格によると

CSS Fonts Module Level 4のWorking Draftを読み解いて、どうなるのか確認しましょう。といっても今回読むべき箇所は多くありません。

4.5. Character range: the unicode-range descriptor

unicode-range 修飾子についての説明が書いてあります。この節の先頭の表を読むと、unicode-range の初期値が U+0-10FFFF であることが分かります。つまり、unicode-range を指定しなかった場合、あらゆる文字をサポートするフォントであるとみなされます。

4.5.1. Using character ranges to define composite fonts

複数のフォントを組み合わせる際の挙動について書いてあります。今回気にしたいのは第2段落の記述です。

If the unicode ranges overlap for a set of @font-face rules with the same family and style descriptor values, the rules are ordered in the reverse order they were defined; the last rule defined is the first to be checked for a given character.

つまり、同じfont-familyかつスタイルの @font-face ルールが複数定義されていて、かつ unicode-range に指定されたコードポイントの範囲が重複している場合、最後に定義された @font-face のフォントから順に試行されてフォールバックすることが規定されています。

実験する

ここまで規格で確認してきた挙動を実験して確かめてみましょう*3

準備

以下のリポジトリを用意しました。

github.com

with-unicode-range.css、without-unicode-range.cssのどちらでもUbuntuフォントを読み込んでいます。また、各HTMLの本文にはキリル文字だけを書いています。with-unicode-range.htmlからはwith-unicode-range.cssを、without-unicode-range.htmlからはwithout-unicode-range.cssをそれぞれ読み込んでいます。

with-unicode-range.cssでは、Ubuntuフォントに対応する @font-face を、先頭から以下の文字種の順に定義しています。視認性を上げるために、各フォントファイルのURLの末尾に文字種をクエリパラメータで入れています。

  • 拡張キリル文字 (cyrillic-ext)
  • キリル文字 (cyrillic)
  • 拡張ギリシャ文字 (greek-ext)
  • ギリシャ文字 (greek)
  • 拡張ラテン文字 (latin-ext)
  • ラテン文字 (latin)

without-unicode-range.cssでは、with-unicode-range.cssの記述から unicode-range を消して、更に拡張キリル文字とキリル文字の定義順を入れ替えてあります。

実験方法

with-unicode-range.html, without-unicode-range.html をそれぞれハード再読み込みします。その際にフォントがどのように読み込まれるのか、ブラウザの開発者ツールで確認します。

実験結果

Chromeの開発者ツールのNetworkタブから、フォントファイル (Fonts) だけに絞り込んだ結果が以下です。

html Networkタブ
with-unicode-range.html f:id:utgwkk:20220324115502p:plain
without-unicode-range.html f:id:utgwkk:20220324115414p:plain

with-unicode-range.htmlではフォントファイル分割の狙いどおりキリル文字のフォントだけが読み込まれています。一方で、without-unicode-range.htmlではラテン文字から順にフォントが読み込まれて、最後にキリル文字のフォントが読み込まれました。

まとめ

Webフォントを分割して読み込む際に unicode-range を指定しなかった場合の挙動については規格で規定されていました。ユーザー体験を向上し、転送量を抑えるためにも @font-face の定義は適切に記述したいですね。

*1:ウェブセーフフォントという。ArialやCourier Newなどが該当するらしい

*2:それぞれGitHubからmainブランチのTTFファイルをダウンロードして確認

*3:実験して挙動を確かめてから規格を読みに行ったので、自分がとった流れとは逆

デスクトップマシンのSSDを取り替える

gyazo.com

Cドライブ (240GB SSD) が埋まった状態でしばらく暮らしていたけど、どうにもならないのでSSDを買った。予備のHDDが挿さってるのでそっちにデータを入れまくっていたけど、WSLのデータは移動しづらくてどんどん容量を食う。

取り替えるのに苦労することもなく、SSDのメーカーに案内されたディスクコピーツールを使ってCドライブをまるっとコピーして差し替えたら完了した。

gyazo.com

何もかもが輝いて見える。パソコンを買い替えるかどうか、という話もあったけどしばらくこれで暮らしてしまいそう。


7年前にも似たような記事を書いている。デスクトップマシンなので今回の方が手順はシンプルだと思うし、そして特筆することもない。

blog.utgw.net