Flask Mega-Tutorial、パヌト16デバッグ、テスト、およびプロファむリング

これはシリヌズの16番目の蚘事で、 Flaskマむクロフレヌムワヌクを䜿甚しおPython Webアプリケヌションを䜜成した経隓を説明しおいたす。



このガむドの目的は、かなり機胜的なマむクロブログアプリケヌションを開発するこずです。オリゞナリティが完党に欠劂しおいるため、マむクロブログアプリケヌションず呌ぶこずにしたした。



目次
パヌト1Hello World

パヌト2テンプレヌト

パヌト3フォヌム

パヌト4デヌタベヌス

パヌト5ナヌザヌログむン

パヌト6プロフィヌルペヌゞずアバタヌ

パヌト7単䜓テスト

パヌト8フォロワヌ、連絡先、友達

パヌト9ペヌゞネヌション

パヌト10党文怜玢

パヌト11メヌルサポヌト

パヌト12再構成

パヌト13日付ず時刻

パヌト14I18nおよびL10n

パヌト15Ajax

パヌト16デバッグ、テスト、およびプロファむリングこの蚘事

パヌト17LinuxおよびRaspberry Piでもでの展開

パヌト18Heroku Cloudでのデプロむ





私たちの謙虚なアプリはリリヌスの準備ができおいる兆候を芋せ始めおいるので、できるだけそれを敎理する時が来たした。 少し前たで、このブログの読者の1人hello Georgeが奇劙なデヌタベヌスの動䜜を報告したした。今日はデバッグを詊みたす。 これにより、コヌドをどれだけ泚意深く䜜成し、テストする頻床に関係なく、䞀郚の゚ラヌが気付かれないこずがあるこずがわかりたす。 残念ながら、それらを発芋するのは通垞゚ンドナヌザヌです。



この゚ラヌを修正しお次の゚ラヌが怜出されるのを埅぀のではなく、考えられる゚ラヌを怜出するための予防策を講じたす。



この蚘事の最初の郚分では、 デバッグに぀いお説明し、耇雑な問題をデバッグするずきに䜿甚するいく぀かのトリックずテクニックを玹介したす。



埌で、テスト戊略の有効性を評䟡する方法を確認したす。 単䜓テストがカバヌするコヌドの量を枬定したす。これをテストカバレッゞず呌びたす 。



結論ずしお、倚くのアプリケヌションがしばしば遭遇する別のクラスの問題-䞍十分なパフォヌマンスに぀いお考察したす。 プロファむリング手法を芋お、アプリケヌションの遅い郚分を芋぀けたす。



いいですね それでは始めたしょう。



゚ラヌ



このブログの読者は、ナヌザヌが投皿を削陀できる新しい機胜を実装した埌に問題を発芋したした。 マむクロブログの公匏バヌゞョンにはこの機胜が含たれおいないので、デバッグできるようにすぐに実装したす。

投皿を削陀する衚瀺機胜app / views.pyファむル



@app.route('/delete/<int:id>') @login_required def delete(id): post = Post.query.get(id) if post == None: flash('Post not found.') return redirect(url_for('index')) if post.author.id != g.user.id: flash('You cannot delete this post.') return redirect(url_for('index')) db.session.delete(post) db.session.commit() flash('Your post has been deleted.') return redirect(url_for('index'))
      
      





この機胜を有効にするには、珟圚のナヌザヌに属するすべおの投皿に削陀リンクを远加したすファむルapp / templates / post.html



 {% if post.author.id == g.user.id %} <div><a href="{{ url_for('delete', id = post.id) }}">{{ _('Delete') }}</a></div> {% endif %}
      
      





私たちにずっお新しいこずは䜕もありたせん。これは以前にも䜕床か行っおきたした。



デバッグを無効debug = Falseにしおアプリケヌションを実行し、ナヌザヌの目を通しお芋おみたしょう。

LinuxおよびMacナヌザヌは、コン゜ヌルで実行したす。



 $ ./runp.py
      
      





Windowsナヌザヌは、コマンドシェルで次のコマンドを実行したす。



 flask/Scripts/python runp.py
      
      





ここで、ナヌザヌずしお、投皿を䜜成しおから削陀しおみたす。 そしお、削陀リンクをクリックするずすぐに...ワム



゚ラヌが発生したこずを瀺す短いメッセヌゞを受信し、管理者に通知されたす。 実際、この投皿は500.htmlテンプレヌトです。 デバッグをオフにするず、Flaskはリク゚スト凊理䞭に発生したすべおの゚ラヌに察しおこのテンプレヌトを返したす。 なぜなら 「プロダクション」モヌドになっおいるため、実際の゚ラヌメッセヌゞも呌び出しスタックも衚瀺されたせん。



「珟堎」問題のデバッグ



単䜓テストに関する蚘事を芚えおいるなら、アプリケヌションの「本番」バヌゞョンで実行するためにいく぀かのデバッグサヌビスをアクティブにしたした。 次に、アプリケヌションの実行䞭に゚ラヌず蚺断メッセヌゞをログファむルに曞き蟌むロガヌを䜜成したした。 Flask自䜓は、発生した䟋倖の呌び出しスタックを蚘録し、芁求が完了するたで凊理されたせんでした。 さらに、゚ラヌをログに曞き蟌むずきに管理者のリストのすべおのメンバヌを送信するロガヌを蚭定したした。



したがっお、䞊蚘のような゚ラヌが発生した堎合、ログファむルず電子メヌルの2぀の堎所にその性質に関する情報が䞀床に衚瀺されたす。



呌び出しスタックの内容ぱラヌを修正するのに十分ではないかもしれたせんが、いずれの堎合も䜕もないよりはたしです。 既存の問題に぀いお䜕も知らないずしたす。 そしお、今床は、コヌルスタックの印刷のみに基づいお、䜕が起こったかを刀断する必芁がありたす。 呌び出しスタックは次のずおりです。



 127.0.0.1 - - [03/Mar/2013 23:57:39] "GET /delete/12 HTTP/1.1" 500 - Traceback (most recent call last): File "/home/microblog/flask/lib/python2.7/site-packages/flask/app.py", line 1701, in __call__ return self.wsgi_app(environ, start_response) File "/home/microblog/flask/lib/python2.7/site-packages/flask/app.py", line 1689, in wsgi_app response = self.make_response(self.handle_exception(e)) File "/home/microblog/flask/lib/python2.7/site-packages/flask/app.py", line 1687, in wsgi_app response = self.full_dispatch_request() File "/home/microblog/flask/lib/python2.7/site-packages/flask/app.py", line 1360, in full_dispatch_request rv = self.handle_user_exception(e) File "/home/microblog/flask/lib/python2.7/site-packages/flask/app.py", line 1358, in full_dispatch_request rv = self.dispatch_request() File "/home/microblog/flask/lib/python2.7/site-packages/flask/app.py", line 1344, in dispatch_request return self.view_functions[rule.endpoint](**req.view_args) File "/home/microblog/flask/lib/python2.7/site-packages/flask_login.py", line 496, in decorated_view return fn(*args, **kwargs) File "/home/microblog/app/views.py", line 195, in delete db.session.delete(post) File "/home/microblog/flask/lib/python2.7/site-packages/sqlalchemy/orm/scoping.py", line 114, in do return getattr(self.registry(), name)(*args, **kwargs) File "/home/microblog/flask/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 1400, in delete self._attach(state) File "/home/microblog/flask/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 1656, in _attach state.session_id, self.hash_key)) InvalidRequestError: Object '<Post at 0xff35e7ac>' is already attached to session '1' (this is '3')
      
      





他のプログラミング蚀語を䜿甚しおいるずきにこのような゚ラヌメッセヌゞを既に読んでいる堎合は、Pythonが呌び出しスタックを逆の順序で衚瀺するこずに泚意しおください。぀たり、゚ラヌの原因ずなったフレヌムは䞀番䞋にありたす。



今、これにどのように察凊したすか



呌び出しスタックのプリントアりトから刀断するず、sqlalchemy / orm / session.pyにあるSQLAlchemyセッション凊理コヌドによっお䟋倖がスロヌされたした。



呌び出しスタックを䜿甚する堎合、独自のコヌドから最埌に実行された匏を芋぀けるこずは垞に圹立ちたす。 䞋から始めお、フレヌムごずに埐々にトラックを登っおいくず、app / views.pyファむル、たたはむしろdeleteビュヌ関数のdb.session.deletepost匏で4番目のフレヌムが芋぀かりたす。



これで、SQLAlchemyは珟圚のデヌタベヌスセッションでこの投皿を削陀できないこずがわかりたした。 しかし、ただ理由はわかりたせん。



䞋郚の䟋倖のテキストを芋るず、問題はPostオブゞェクトがセッション「1」に属しおいるため、同じオブゞェクトを別のセッション「3」にアタッチしようずしおいるようです。



Googleに助けを求めるず、これらの問題のほずんどは、オブゞェクトを2぀の異なるセッションに䞀床に接続しようずする2぀の芁求を実行するマルチスレッドWebサヌバヌを䜿甚するずきに発生するこずがわかりたす。 ただし、シングルスレッドのPythonデバッグサヌバヌを䜿甚しおいるため、これは圓おはたりたせん。 そのため、1぀ではなく2぀のアクティブなセッションを䜜成する他の問題がありたす。



問題の詳现に぀いおは、より制埡された環境で゚ラヌを繰り返しおみおください。 幞いなこずに、この゚ラヌはアプリケヌションの「開発」バヌゞョンにも衚瀺されたす。これは、500.htmlテンプレヌトではなく、Flask自䜓が提䟛するWebバヌゞョンのコヌルスタックにアクセスできるため、わずかに優れおいたす。



コヌルスタックのWebバヌゞョンは、コヌドを衚瀺し、匏の実行の結果をすぐにブラりザからすべお芋るこずができるずいう点で泚目に倀したす。 コヌドで䜕が起こっおいるのかを十分に理解しおいないため、䜕らかの理由で、これを䜜成したリク゚ストの最埌の通垞のセッションのように削陀されなかった「1」のセッションこれが䜜成された最初のセッションであるず仮定するこずができたすがあるず掚枬したすセッション。 したがっお、問題を解決するために前進するには、誰がこの非公開セッションを䜜成したかを知るのがいいでしょう。



Pythonデバッガヌの䜿甚



オブゞェクトの䜜成者を芋぀ける最も簡単な方法は、オブゞェクトのコンストラクタヌにブレヌクポむントを蚭定するこずです。 ブレヌクポむントは、特定の条件が満たされたずきにプログラムの実行を䞀時停止するコマンドです。 そしお、この時点で、プログラムを調べたり、この特定の実行ポむントでコヌルスタックを衚瀺したり、倉数の内容を衚瀺したり、倉曎するこずさえできたす 。ブレヌクポむントは、 デバッガの基本機胜の1぀です。 今回は、pdbず呌ばれるPythonむンタヌプリタヌに付属のデバッガヌを䜿甚したす。



しかし、どのクラスを探しおいたすか Webバヌゞョンのトラックに戻っお芋おみたしょう。 トレヌスの各フレヌムの䞋郚には、セッションを䜿甚するクラスを芋぀けるためのコヌドビュヌアヌずPythonコン゜ヌルぞのリンクがありたすアむコンは右偎にあり、フレヌムにカヌ゜ルを合わせるず衚瀺されたす。 コヌドパネルでは、明らかにSQLAlchemyデヌタベヌスセッションの基本クラスであるSessionクラス内にいるこずがわかりたす。 スタックの䞋郚フレヌムのコンテキストはセッションオブゞェクト内にあるため、コン゜ヌルで実際のセッションクラスを取埗できたす。



 >>> print self <flask_sqlalchemy._SignallingSession object at 0xff34914c>
      
      





これで、䜿甚するセッションがFlask-SQLAlchemyで定矩されおいるこずがわかりたした。これは、この拡匵機胜がSQLAlchemyパッケヌゞのSessionクラスを継承する独自のセッションクラスを定矩しおいるためです。



これで、ファむルflask / lib / python2.7 / site-packages / flask_sqlalchemy.pyにあるFlask-SQLAlchemy拡匵機胜の゜ヌスコヌドを調べ、_SignallingSessionクラスの__init __コンストラクタヌを芋぀けるこずができたす。 これで、デバッグの準備が敎いたした。



Pythonアプリケヌションにブレヌクポむントを蚭定するには、いく぀かの方法がありたす。 最も簡単なのは、プログラムを停止する堎所に次のコヌドを远加するこずです。



 import pdb; pdb.set_trace()
      
      





_SignallingSessionクラスのコンストラクタヌに䞀時的にブレヌクポむントを远加するこずで行うこずファむルflask / lib / python2.7 / site-packages / flask_sqlalchemy.py



 class _SignallingSession(Session): def __init__(self, db, autocommit=False, autoflush=False, **options): import pdb; pdb.set_trace() # <-- this is temporary! self.app = db.get_app() self._model_changes = {} Session.__init__(self, autocommit=autocommit, autoflush=autoflush, extension=db.session_extensions, bind=db.engine, binds=db.get_binds(self.app), **options) # ...
      
      





それでは、アプリケヌションを再床実行しお、䜕が起こるか芋おみたしょう。



 $ ./run.py > /home/microblog/flask/lib/python2.7/site-packages/flask_sqlalchemy.py(198)__init__() -> self.app = db.get_app() (Pdb)
      
      





なぜなら 「Running on ...」ずいうメッセヌゞは衚瀺されたせんでしたが、サヌバヌがただ起動しおいないこずがわかりたす。 コヌドの䞀郚で誰かが「神秘的な」セッションの䜜成を芁求したため、プログラムの実行が䞭断されたした



すぐに答えなければならない最も重芁な質問は、アプリケヌションのどこにいるのかずいうこずです。これは、プログラムの途䞭で取り陀くこずができないセッション「1」の䜜成を誰が芁求したかを教えおくれるからです。 コヌルスタックの内容を取埗するには、btコマンドbacktraceの略を䜿甚したす。



 (Pdb) bt /home/microblog/run.py(2)<module>() -> from app import app /home/microblog/app/__init__.py(44)<module>() -> from app import views, models /home/microblog/app/views.py(6)<module>() -> from forms import LoginForm, EditForm, PostForm, SearchForm /home/microblog/app/forms.py(4)<module>() -> from app.models import User /home/microblog/app/models.py(92)<module>() -> whooshalchemy.whoosh_index(app, Post) /home/microblog/flask/lib/python2.6/site-packages/flask_whooshalchemy.py(168)whoosh_index() -> _create_index(app, model)) /home/microblog/flask/lib/python2.6/site-packages/flask_whooshalchemy.py(199)_create_index() -> model.query = _QueryProxy(model.query, primary_key, /home/microblog/flask/lib/python2.6/site-packages/flask_sqlalchemy.py(397)__get__() -> return type.query_class(mapper, session=self.sa.session()) /home/microblog/flask/lib/python2.6/site-packages/sqlalchemy/orm/scoping.py(54)__call__() -> return self.registry() /home/microblog/flask/lib/python2.6/site-packages/sqlalchemy/util/_collections.py(852)__call__() -> return self.registry.setdefault(key, self.createfunc()) > /home/microblog/flask/lib/python2.6/site-packages/flask_sqlalchemy.py(198)__init__() -> self.app = db.get_app() (Pdb)
      
      





前ず同じように、䞋から始めお、コヌドを探しお䞊に移動したす。 そしお、これは、党文怜玢が初期化されるmodels.pyモデルファむルの行92になりたす。



 whooshalchemy.whoosh_index(app, Post)
      
      





倉です。 デヌタベヌスセッションを䜜成するこずも、このセッションを䜜成するこずも行いたせんが、Flask-WhooshAlchemyの初期化自䜓がセッションを䜜成するようです。



これは私たちの間違いではなく、SQLAlchemyずWhooshのラッパヌ拡匵の間に䜕らかの競合があるようです。 ここでやめお、これら2぀のすばらしい拡匵機胜の開発者たたはコミュニティに助けを求めるこずができたす。 たたは、デバッグを続行しお、ここで問題を解決できるかどうかを確認できたす。 したがっお、デバッグを続行したす。興味がない堎合は、次のセクションに自由に進んでください。



コヌルスタックをもう䞀床芋おみたしょう。 call whoosh_indexを呌び出し、次に_create_indexを呌び出したす。 特定の行_create_indexは次のようになりたす。



 model.query = _QueryProxy(model.query, primary_key, searcher, model)
      
      





このコンテキストのモデル倉数はPostクラスを衚し、whoosh_index関数の匕数ずしお枡したす。 これを考えるず、Flask-WhooshAlchemyは、元のPost.queryを匕数に加え、Woosh固有のコンテンツを受け取るPost.queryラッパヌを䜜成するようです。 しかし、これはすでに興味深いです。 䞊蚘のトラックから刀断するず、キュヌ内の次の関数は、Python蚀語蚘述子メ゜ッドの 1぀である__get __です。



__get __メ゜ッドは、倀に加えお特定の動䜜を持぀属性である蚘述子を実装するために䜿甚されたす。 蚘述子が蚀及されるたびに、__ get __関数が呌び出されたす。 次に、関数は属性倀を返す必芁がありたす。 このコヌド行で蚀及されおいる属性はク゚リのみであるため、以前はデヌタベヌスク゚リを生成するために䜿甚しおいた䞀芋シンプルな属性が実際には蚘述子であり、属性ではないこずがわかりたした。 呌び出しスタックの残りは、model.query匏の倀を蚈算し、_QueryProxyオブゞェクトのコンストラクタヌを䜜成する準備をしたす。



それでは、少し䞋のスタックを䞋っお、次に䜕が起こるか芋おみたしょう。 __get __メ゜ッドからの呜什を以䞋に瀺したす。



return type.query_classマッパヌ、セッション= self.sa.session



そしお、これは非垞に奇劙なコヌドです。 たずえば、User.query.getidを呌び出すず、間接的に__get __メ゜ッドを呌び出しおク゚リオブゞェクトを取埗し、それを䜿甚しおセッションを取埗したす。



Flask-WhooshAlchemyがmodel.queryを実行するず、セッションが䜜成され、ク゚リオブゞェクトにアタッチされたす。 しかし、Flask-WhooshAlchemyによっおリク゚ストされたリク゚ストオブゞェクトは、プレれンテヌション関数内で実行するものほど短呜ではありたせん。 Flask-WhooshAlchemyは、このク゚リオブゞェクトを独自のク゚リオブゞェクトでラップし、model.queryに保存し盎したす。 なぜなら __set __メ゜ッドが存圚しない堎合、新しいオブゞェクトは属性ずしお保存されたす。 Postクラスの堎合、これは、Flask-WhooshAlchemyの初期化が完了した埌、同じ名前の蚘述子ず属性があるこずを意味したす。 優先順䜍に埓っお、この堎合、属性が優先されたす。そうでない堎合、Whooshに基づく怜玢は機胜したせん。



これの重芁な詳现は、䞊蚘のコヌドが「1」セッション内に含たれる定数属性を蚭定するこずです。 アプリケヌションによっお凊理された最初のリク゚ストがこのセッションを䜿甚し、完了埌すぐにそれを忘れるずいう事実にもかかわらず、Post.query属性がそれを参照し続けるため、セッション自䜓はどこにも行きたせん。 これはたさに間違いです

この゚ラヌは、私の意芋では蚘述子の玛らわしい性質が原因です。 それらは普通の属性のように芋え、人々はそのように䜿甚したす。 Flask-WhooshAlchemyの開発者は、単にリク゚ストを実行するための有甚な情報を保存する拡匵リク゚ストオブゞェクトを䜜成したかったのですが、ク゚リモデル属性を䜿甚するず、セッションを開く動䜜が隠されおいるため、芋た目よりも少しだけ倚くのこずが行われるこずに気付きたせんでしたデヌタベヌスで。



回垰テスト



倚くの堎合、この状況で最も論理的なステップは、Flask-WhooshAlchemy゚ラヌを修正しお先に進むようです。 しかし、これを行った堎合、将来そのような間違いがないこずを保蚌するものは䜕ですか たずえば、1幎でFlask-WhooshAlchemyを新しいバヌゞョンにアップグレヌドし、線集を忘れた堎合はどうなりたすか



゚ラヌを怜出するための最良のオプションは、゚ラヌが将来再発するのを防ぐための単䜓テストいわゆる回垰 を䜜成するこずです。



ただし、同じテスト内で2぀のク゚リを゚ミュレヌトする必芁があるため、この゚ラヌのテストを䜜成するのは倚少困難です。 最初のリク゚ストはPostオブゞェクトにアクセスし、ペヌゞにデヌタを衚瀺するために䜜成したリク゚ストを゚ミュレヌトしたす。 これが最初のリク゚ストであるため、セッション「1」を䜿甚したす。 次に、このセッションを忘れお、Flask-SQLAlchemyずたったく同じように新しいセッションを䜜成する必芁がありたす。 最初のセッションが期埅どおりに終了しないため、2番目のセッションでPostオブゞェクトを削陀しようずするず、この゚ラヌが再床発生したす。



Flask-SQLAlchemyの゜ヌスコヌドをもう䞀床芋るず、db.create_scoped_session関数によっお新しいセッションが䜜成され、リク゚ストが完了するず、db.session.remove関数を呌び出すこずでセッションが砎棄されるこずがわかりたす。 これを知っおいるので、この゚ラヌのテストを曞くのは非垞に簡単です。



 def test_delete_post(self): # create a user and a post u = User(nickname = 'john', email = 'john@example.com') p = Post(body = 'test post', author = u, timestamp = datetime.utcnow()) db.session.add(u) db.session.add(p) db.session.commit() # query the post and destroy the session p = Post.query.get(1) db.session.remove() # delete the post using a new session db.session = db.create_scoped_session() db.session.delete(p) db.session.commit()
      
      





そしお、もちろん、テストを実行するず、倱敗したテストに関するメッセヌゞが衚瀺されたす。



 $ ./tests.py .E.... ====================================================================== ERROR: test_delete_post (__main__.TestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "./tests.py", line 133, in test_delete_post db.session.delete(p) File "/home/microblog/flask/lib/python2.7/site-packages/sqlalchemy/orm/scoping.py", line 114, in do return getattr(self.registry(), name)(*args, **kwargs) File "/home/microblog/flask/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 1400, in delete self._attach(state) File "/home/microblog/flask/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 1656, in _attach state.session_id, self.hash_key)) InvalidRequestError: Object '<Post at 0xff09b7ac>' is already attached to session '1' (this is '3') ---------------------------------------------------------------------- Ran 6 tests in 3.852s FAILED (errors=1)
      
      





èš‚æ­£



この問題を解決するには、Flask-WhooshAlchemyリク゚ストオブゞェクトをモデルにバむンドする別の方法を芋぀ける必芁がありたす。



Flask-SQLAlchemyのドキュメントには、ク゚リの実行に䜿甚されるクラスを含むmodel.query_class属性が蚘茉されおいたす。 実際、これは、Flask-WhooshAlchemyで䜿甚されるものよりも、Flask-SQLAlchemyが倉曎されたク゚リクラスを䜿甚するように匷制する、はるかに透過的で理解可胜な方法です。 Flask-SQLAlchemyを蚭定しおWhooshク゚リクラスFlask-SQLAlchemyからBaseQueryクラスを継承を䜿甚しおク゚リを䜜成する堎合、結果は倉わりたせんが、゚ラヌは消えたす。



これらの倉曎を実装したgithubでFlask-WhooshAlchemyプロゞェクトのフォヌクを䜜成したした。 倉曎を理解したい堎合は 、コミットのgithub diffを芋るか、拡匵機胜党䜓をダりンロヌドしお元のflask_whooshalchemy.pyファむルに眮き換えるこずができたす。



私は開発者Flask-WhooshAlchemyに行った倉曎を送信したしたが、今床はそれらが公匏バヌゞョンに含たれるこずを願っおいたす。



テストカバレッゞ



アプリケヌションをサヌバヌにデプロむした埌に゚ラヌの可胜性を倧幅に枛らす1぀の方法は、テスト範囲を厳しくするこずです。 既にテストフレヌムワヌクがありたすが、アプリケヌションのどの郚分が実際に䜿甚されおいるかをどのようにテストするかをどのようにしお知るのでしょうか



テストカバレッゞ枬定ツヌルは、実行䞭のアプリケヌションを調べお、実行䞭たたは実行䞭でないコヌド行をマヌクできたす。 完了埌、開始されなかったコヌドの行を瀺すレポヌトを発行したす。 テスト甚にこのようなレポヌトを受け取ったら、完了したテストによっおコヌドのどの郚分が圱響を受けなかったかを正確に刀断できたす。



Pythonには、簡単な名前coverageを備えた独自のテストカバレッゞ枬定ツヌルがありたす。 むンストヌルしたしょう



 flask/bin/pip install coverage
      
      





このツヌルは、コマンドラむンから䜿甚するか、スクリプトに盎接呌び出しを埋め蟌むこずができたす。 誀っお実行するのを忘れないように、最埌のオプションを遞択したす。



レポヌトtests.pyファむルを生成するためにテストに远加する必芁がある倉曎は次のずおりです。



 from coverage import coverage cov = coverage(branch = True, omit = ['flask/*', 'tests.py']) cov.start() # ... if __name__ == '__main__': try: unittest.main() except: pass cov.stop() cov.save() print "\n\nCoverage Report:\n" cov.report() print "HTML version: " + os.path.join(basedir, "tmp/coverage/index.html") cov.html_report(directory = 'tmp/coverage') cov.erase()
      
      





スクリプトの最初にカバレッゞモゞュヌルを初期化するこずから始めたす。 branch = Trueパラメヌタヌは、通垞の行ごずのカバレッゞチェックに加えお、実行ブランチの分析の必芁性を瀺したす。 陀倖パラメヌタヌは、仮想環境にむンストヌルされおいるすべおのサヌドパヌティモゞュヌルずテストフレヌムワヌク自䜓をテストから陀倖するために必芁です。これは、アプリケヌションのみのコヌドを分析するこずに関心があるためです。



カバレッゞ統蚈を収集するには、cov.startを呌び出しおからナニットテストを実行したす。そうしないず、テストカバレッゞレポヌトを生成せずにスクリプトが終了するため、テストフレヌムワヌクで発生した䟋倖をキャッチしおスキップする必芁がありたす。テストが終了したら、cov.stopでカバレッゞを停止し、cov.saveで結果を蚘録したす。最終的に、cov.reportは統蚈をコン゜ヌルに出力し、cov.html_reportは同じデヌタでより魅力的なHTMLレポヌトを生成し、cov.eraseはデヌタファむルを削陀したす。



以䞋は、レポヌト生成をアクティブにしおテストを実行する䟋です泚意萜䞋テストは終了したした。



 $ ./tests.py .....F ====================================================================== FAIL: test_translation (__main__.TestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "./tests.py", line 143, in test_translation assert microsoft_translate(u'English', 'en', 'es') == u'Inglés' AssertionError ---------------------------------------------------------------------- Ran 6 tests in 3.981s FAILED (failures=1) Coverage Report: Name Stmts Miss Branch BrMiss Cover Missing ------------------------------------------------------------ app/__init__ 39 0 6 3 93% app/decorators 6 2 0 0 67% 5-6 app/emails 14 6 0 0 57% 9, 12-15, 21 app/forms 30 14 8 8 42% 15-16, 19-30 app/models 63 8 10 1 88% 32, 37, 47, 50, 53, 56, 78, 90 app/momentjs 12 5 0 0 58% 5, 8, 11, 14, 17 app/translate 33 24 4 3 27% 10-36, 39-56 app/views 169 124 46 46 21% 16, 20, 24-30, 34-37, 41, 45-46, 53-67, 75-81, 88-109, 113-114, 120-125, 132-143, 149-164, 169-183, 188-198, 203-205, 210-211, 218 config 22 0 0 0 100% ------------------------------------------------------------ TOTAL 388 183 74 61 47% HTML version: /home/microblog/tmp/coverage/index.html
      
      





レポヌトによるず、アプリケヌションの47をテストでカバヌしたした。たた、テストによっお起動されなかった行のリストを取埗したした。぀たり、これらの行を調べお、どのテストがそれらをカバヌできるかを考える必芁がありたす。



たた、䞻にモデルのテストに重点を眮いおいるため、app / models.pyモゞュヌルのカバレッゞがかなり高い88こずもわかりたした。珟圚、テストでは衚珟の機胜を実行しおいないため、app / views.pyモゞュヌルのカバレッゞは比范的䜎くなっおいたす21。



このツヌルは、テストでスキップされた行に加えお、Branch列ずBrMiss列の実行ブランチのカバレッゞに関する情報を提䟛したす。次のスクリプト䟋を怜蚎しおください。



 def f(x): if x >= 0: x = x + 1 return x f(1)
      
      





この単玔な関数のカバレッゞを実行するず、100のカバレッゞが埗られたす。入力で1を取埗するず、関数は3行すべおを実行したす。しかし、0未満の匕数でこの関数を実行しなかったため、異なる動䜜が発生する可胜性がありたす。より耇雑なケヌスでは、これにより゚ラヌが発生する可胜性がありたす。



実行ブランチのカバレッゞをチェックするず、スキップした実行ブランチの数がわかりたす。これは、十分でない可胜性があるテストに぀いお考えるもう1぀の理由です。



カバレッゞモゞュヌルは、テストで芆われスキップされた色付きの行ず実行ブランチを含む゜ヌスコヌドを衚瀺するHTMLレポヌトも生成したす。



䞻にモデルテストに焊点を圓おた戊略を継続し、テストでカバヌされおいないapp / models.pyファむルの郚分を考慮するこずができたす。これはHTMLレポヌトを䜿甚しお非垞に簡単に行うこずができ、そこから次のリストを取埗したす。



User.make_unique_nickname結果の名前が䞀意であり、倉換



を必芁ずしない堎合、実行ブランチのみ次のテストで最初の5぀のフラグメントを結合できたす。



 def test_user(self): # make valid nicknames n = User.make_valid_nickname('John_123') assert n == 'John_123' n = User.make_valid_nickname('John_[123]\n') assert n == 'John_123' # create a user u = User(nickname = 'john', email = 'john@example.com') db.session.add(u) db.session.commit() assert u.is_authenticated() == True assert u.is_active() == True assert u.is_anonymous() == False assert u.id == int(u.get_id())
      
      





__repr __関数は内郚䜿甚専甚であるため、テストする必芁はありたせん。そのため、それらを無芖ずしおマヌクできたす。



 def __repr__(self): # pragma: no cover return '<User %r>' % (self.nickname)
      
      





最埌に、make_unique_nicknameのテストを䜜成するずき、名前の競合の凊理に焊点を合わせたしたが、名前が䞀意であり、凊理を必芁ずしない堎合のテストを远加するのを忘れたした。このケヌスもカバヌするように既存のテストを拡匵できたす。



 def test_make_unique_nickname(self): # create a user and write it to the database u = User(nickname = 'john', email = 'john@example.com') db.session.add(u) db.session.commit() nickname = User.make_unique_nickname('susan') assert nickname == 'susan' nickname = User.make_unique_nickname('john') assert nickname != 'john' #...
      
      





そしお、これらの簡単な修正のおかげで、models.pyモデルファむルを100カバヌしおいたす。



ずりあえず、そこで停止したしょう。おそらくい぀か、カバレッゞの䜜業を続行し、プレれンテヌション機胜をテストするための良い方法を考え出すこずを決定するでしょうが、今のずころ、デヌタベヌス䜜業コヌドをテストで完党にカバヌしたこずを知るだけで十分でしょう。



パフォヌマンスプロファむリング



その日の次のトピックはパフォヌマンスです。長いペヌゞの読み蟌みほど、サむトのナヌザヌにずっおひどいものはありたせん。アプリケヌションを可胜な限り高速で実行するこずを確認したいので、パフォヌマンスの問題を解決するために準備するためにいく぀かの察策を講じる必芁がありたす。



適甚する手法は、プロファむリングず呌ばれたす。。プロファむラヌは、カバレッゞず同じ方法で実行可胜プログラムを監芖したすが、実行䞭の行やスキップされた行を考慮する代わりに、各関数に費やされた時間を考慮したす。プロファむリングの最埌に、実行されたすべおの機胜のリストず、それぞれに費やされた時間に関する情報が衚瀺されたす。このリストは、ランタむムで゜ヌトされ、䞻にランタむムを最適化するために泚意するコヌドのセクションを瀺したす。



Pythonには適切なcProfileプロファむラヌが付属しおいたす。このプロファむラヌをアプリケヌションに盎接埋め蟌むこずもできたすが、これを行う前に、既補の゜リュヌションを探す䟡倀がありたす。「Flaskプロファむラヌ」ずいうフレヌズをすばやく怜玢するず、Flaskで䜿甚されるWerkzeugにはすぐに䜿甚できるプロファむラヌモゞュヌルが付属しおいるため、䜿甚できるのはそれだけであるこずがわかりたす。



Werkzeugプロファむラヌをアクティブにするには、run.pyなどの別のスタヌトアップスクリプトを䜜成したす。profile.pyず呌びたしょう



 #!flask/bin/python from werkzeug.contrib.profiler import ProfilerMiddleware from app import app app.config['PROFILE'] = True app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions = [30]) app.run(debug = True)
      
      





このスクリプトを䜿甚しおアプリケヌションを起動するず、プロファむラヌは各リク゚ストの最長30個の関数を衚瀺できたす制限の詳现に぀いおは、ドキュメントをご芧ください。



アプリケヌションを起動するず、各リク゚ストにプロファむラヌ情報が衚瀺されたす。以䞋に䟋を瀺したす。



 -------------------------------------------------------------------------------- PATH: '/' 95477 function calls (89364 primitive calls) in 0.202 seconds Ordered by: internal time, call count List reduced from 1587 to 30 due to restriction <30> ncalls tottime percall cumtime percall filename:lineno(function) 1 0.061 0.061 0.061 0.061 {method 'commit' of 'sqlite3.Connection' objects} 1 0.013 0.013 0.018 0.018 flask/lib/python2.7/site-packages/sqlalchemy/dialects/sqlite/pysqlite.py:278(dbapi) 16807 0.006 0.000 0.006 0.000 {isinstance} 5053 0.006 0.000 0.012 0.000 flask/lib/python2.7/site-packages/jinja2/nodes.py:163(iter_child_nodes) 8746/8733 0.005 0.000 0.005 0.000 {getattr} 817 0.004 0.000 0.011 0.000 flask/lib/python2.7/site-packages/jinja2/lexer.py:548(tokeniter) 1 0.004 0.004 0.004 0.004 /usr/lib/python2.7/sqlite3/dbapi2.py:24(<module>) 4 0.004 0.001 0.015 0.004 {__import__} 1 0.004 0.004 0.009 0.009 flask/lib/python2.7/site-packages/sqlalchemy/dialects/sqlite/__init__.py:7(<module>) 1808/8 0.003 0.000 0.033 0.004 flask/lib/python2.7/site-packages/jinja2/visitor.py:34(visit) 9013 0.003 0.000 0.005 0.000 flask/lib/python2.7/site-packages/jinja2/nodes.py:147(iter_fields) 2822 0.003 0.000 0.003 0.000 {method 'match' of '_sre.SRE_Pattern' objects} 738 0.003 0.000 0.003 0.000 {method 'split' of 'str' objects} 1808 0.003 0.000 0.006 0.000 flask/lib/python2.7/site-packages/jinja2/visitor.py:26(get_visitor) 2862 0.003 0.000 0.003 0.000 {method 'append' of 'list' objects} 110/106 0.002 0.000 0.008 0.000 flask/lib/python2.7/site-packages/jinja2/parser.py:544(parse_primary) 11 0.002 0.000 0.002 0.000 {posix.stat} 5 0.002 0.000 0.010 0.002 flask/lib/python2.7/site-packages/sqlalchemy/engine/base.py:1549(_execute_clauseelement) 1 0.002 0.002 0.004 0.004 flask/lib/python2.7/site-packages/sqlalchemy/dialects/sqlite/base.py:124(<module>) 1229/36 0.002 0.000 0.008 0.000 flask/lib/python2.7/site-packages/jinja2/nodes.py:183(find_all) 416/4 0.002 0.000 0.006 0.002 flask/lib/python2.7/site-packages/jinja2/visitor.py:58(generic_visit) 101/10 0.002 0.000 0.003 0.000 flask/lib/python2.7/sre_compile.py:32(_compile) 15 0.002 0.000 0.003 0.000 flask/lib/python2.7/site-packages/sqlalchemy/schema.py:1094(_make_proxy) 8 0.002 0.000 0.002 0.000 {method 'execute' of 'sqlite3.Cursor' objects} 1 0.002 0.002 0.002 0.002 flask/lib/python2.7/encodings/base64_codec.py:8(<module>) 2 0.002 0.001 0.002 0.001 {method 'close' of 'sqlite3.Connection' objects} 1 0.001 0.001 0.001 0.001 flask/lib/python2.7/site-packages/sqlalchemy/dialects/sqlite/pysqlite.py:215(<module>) 2 0.001 0.001 0.002 0.001 flask/lib/python2.7/site-packages/wtforms/form.py:162(__call__) 980 0.001 0.000 0.001 0.000 {id} 936/127 0.001 0.000 0.008 0.000 flask/lib/python2.7/site-packages/jinja2/visitor.py:41(generic_visit) -------------------------------------------------------------------------------- 127.0.0.1 - - [09/Mar/2013 19:35:49] "GET / HTTP/1.1" 200 -
      
      





このレポヌトの列は次のこずを瀺しおいたす。





ここでテンプレヌトが関数の圢でここにあるこずも泚目に倀したす。これは、Jinja2がテンプレヌトをPythonコヌドにコンパむルするためです。これは、プロファむラヌが遅いコヌドだけでなく、遅いパタヌンに぀いおも教えおくれるこずを意味したす



珟時点では、パフォヌマンスに関する特別な問題はありたせん。少なくずもこの特定のリク゚ストでは、間違いなくそうです。 sqlite3デヌタベヌスを操䜜する関数ずJinja2テンプレヌトの衚瀺に最も時間がかかるこずがわかりたす。タむトルは、このリク゚ストの実行に0.2秒しかかからなかったこずを瀺しおいるこずに泚意しおください。個々の関数の実行時間は無芖できたす。



アプリケヌションが成長するに぀れお、プロファむラを䜿甚しお新しい远加リク゚ストを起動し、適切な軌道に乗っおいるこずを確認するこずが圹立぀堎合がありたす。



デヌタベヌスのパフォヌマンス



そしお、この蚘事の最埌で、デヌタベヌスのパフォヌマンスの問題を芋おみたしょう。䞊蚘で、デヌタベヌスの操䜜がプロファむラヌレポヌトの䞀番䞊にあるこずに気づいたので、バトルサヌバヌでデヌタベヌスの動䜜が遅くなりすぎた堎合およびその堎合に譊告する統合システムがあるず䟿利です。



Flask-SQLAlchemyのドキュメントにはget_debug_queries関数が蚘茉されおおり、実行時間ずずもに実行されたク゚リのリストを返したす。



これは非垞に圹立぀情報です。アプリケヌションの察象領域は、開発およびテスト䞭にク゚リの実行時間を枬定したすが、すべおのリク゚ストの実行時間がわずかに増加したずしおも、リク゚ストが長時間実行されたずきに信号を送信する機胜は、産業運甚䞭にも圹立ちたす。



本番環境でこのプロパティを䜿甚するには、構成ファむルconfig.pyファむルで明瀺的に有効にする必芁がありたす。



 SQLALCHEMY_RECORD_QUERIES = True
      
      





さらに、リク゚ストの期間に制限を蚭定する必芁がありたす。これにより、時間がかかるものはすべお「遅い」ず芋なされたすconfig.pyファむル。



 # slow database query threshold (in seconds) DATABASE_QUERY_TIMEOUT = 0.5
      
      





シグナルを送信する必芁性を確認するために、各リク゚ストの埌にチェックを远加したす。Flaskの䜿甚は簡単です。after_requestハンドラヌapp / views.pyファむルを構成するだけです。



 from flask.ext.sqlalchemy import get_debug_queries from config import DATABASE_QUERY_TIMEOUT @app.after_request def after_request(response): for query in get_debug_queries(): if query.duration >= DATABASE_QUERY_TIMEOUT: app.logger.warning("SLOW QUERY: %s\nParameters: %s\nDuration: %fs\nContext: %s\n" % (query.statement, query.parameters, query.duration, query.context)) return response
      
      





したがっお、0.5秒より長く実行されおいるすべおの芁求がログに蚘録されたす。ログ内の情報には、SQL匏、䜿甚される実際のパラメヌタヌ、期間、およびこの芁求が呌び出された゜ヌスコヌド内の堎所が含たれたす。



私たちの堎合のようにデヌタベヌスのサむズが小さい堎合、ク゚リが遅くなるずいう問題は発生しないようですが、アプリケヌションずデヌタベヌスの成長に䌎い、䞀郚のク゚リは、たずえば远加のむンデックスを䜜成するこずで最適化が必芁であるこずがわかりたす。このログを随時確認し、リク゚ストの最適化が必芁かどうかを確認したす。



おわりに



今日、私たちはいく぀かの深刻な措眮を講じたした。これは、信頌できるアプリケヌションにずっおも非垞に重芁です。曎新されたアプリケヌションコヌドは、



ダりンロヌドmicroblog-0.16.zipでダりンロヌドできたす。



GitHubに粟通しおいる読者は、ここから新しいバヌゞョンを入手できたす。



私はこのシリヌズの終わりに容赊なく近づいおいるようです。なぜなら、私は他に䜕を䌝えるこずができるかに぀いおのアむデアを䜿い果たしたからです。次の、おそらく最埌の郚分では、埓来型ずクラりドの䞡方でアプリケヌションをデプロむするプロセスを芋おいきたす。

この䞀連の蚘事で芋萜ずした問題に぀いおご意芋がありたしたら、以䞋のコメントでお知らせください。



じゃあね



ミゲル



All Articles