From 81412b8898fd2d6b148cf7f7079d916c0ca8e961 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 24 May 2023 16:41:45 +0200 Subject: [PATCH] tex: add stuff on integration testing --- tex/text.tex | 143 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 137 insertions(+), 6 deletions(-) diff --git a/tex/text.tex b/tex/text.tex index af59e9e..59b7104 100644 --- a/tex/text.tex +++ b/tex/text.tex @@ -1150,12 +1150,143 @@ intended/expected. \n{2}{Integration tests} -Integrating with external software, namely the database in case of this program -is designed to utilise the same mechanism that was mentioned in the previous -section: Go's \texttt{testing} package. These test verify that the code changes -can still perform the same actions with the external software that were -possible before the change and are equally run before every commit locally and -then in the CI. +Integrating with external software, namely the database in case of this +program, is designed to utilise the same mechanism that was mentioned in the +previous section: Go's \texttt{testing} package. These tests verify that the +code changes can still perform the same actions with the external software that +were possible before the change and are run before every commit locally and +then after pushing to remote in the CI. + +\n{3}{func TestUserExists(t *testing.T)} + +An example integration test shown in Figure~\ref{fig:integrationtest} can be +seen to declare a helper function \texttt{getCtx() context.Context}, which +takes no arguments and returns a new \texttt{context.Context} initialised with +a value of the global logger, which is how the logger gets injected into the +user module functions. The function \texttt{TestUserExists(t *testing.T)} first +declares a database connection string and attempting to open a connection to +the database. The database in use here is SQLite3 running in memory mode, +meaning no file is actually written to disk during this process. Since the +testing data is not needed after the test, this is deemed good enough. Next, a +defer statement calling the \texttt{Close()} method on the database object is +made, which is the idiomatic Go way of closing files and network connections +(which are also an abstraction over files on UNIX-like operating systems such +as GNU/Linux). The \emph{defer} statement gets called when after all of the +statements in the surrounding function, which makes sure no file descriptors +(FDs) are leaked and the file is properly closed when the function returns. + +In the next step a database schema creation is attempted, handling the +potential error in a Go idiomatic way, which uses the return value from the +function in an assignment to a variable declared in the \texttt{if} statement, +and checks whether the \texttt{err} was \texttt{nil} or not. In case the +\texttt{err} was not \texttt{nil}, i.e.\ \emph{there was an error in the callee +function}, the condition evaluates to \texttt{true}, which is followed by +entering the inner block. Inside the inner block the error is announced to the +user (likely a developer running the test in this case) and the testing +object's \texttt{FailNow()} method is called, which marks the test function as +having failed and stops its execution, which in this case is the desired +outcome, since if the database schema creation call fails there really is no +point in continuing testing of user creation. + +Conversely, if the schema does get created without an error, the code continues +to declare a few variables: \texttt{username}, \texttt{email} and \texttt{ctx}, +to which the context injected with the logger is saved. Some of them are +subsequently passed into the \texttt{UsernameExists} function, context as the +first argument, with the database pointer and username being passed next, while +the \texttt{email} variable is only used at a later stage, but was declared +here to give a sense of grouping. The error value returned from this function +is again checked and if everything goes well, the value of the +\texttt{usernameFound} boolean is checked next. Since the database has just +been created, there should be no users, which is checked in the next +\texttt{if} statement. The same check is then performed for the +earlier-declared user email that is also expected to fail. + +The final statements of the described test attempts a user creation call, which +is again checked for both error and \emph{nilability}. The test continues with +more similar checks but it has been cut short for brevity. + +A neat thing about error handling in Go is it allows for very easy checking of +all paths, not just the \emph{happy path} where there are no issues. + +\begin{figure}[!h] + \centering + \scriptsize + \begin{varwidth}{\linewidth} + \begin{verbatim} + // modules/user/user_test.go + package user + + import ( + "context" + "testing" + + "git.dotya.ml/mirre-mt/pcmt/ent/enttest" + "git.dotya.ml/mirre-mt/pcmt/slogging" + _ "github.com/xiaoqidun/entps" + ) + + func getCtx() context.Context { + l := slogging.Init(false) + ctx := context.WithValue(context.Background(), CtxKey{}, l) + + return ctx + } + + func TestUserExists(t *testing.T) { + connstr := "file:ent_tests?mode=memory&_fk=1" + db := enttest.Open(t, "sqlite3", connstr) + defer db.Close() + + if err := db.Schema.Create(context.Background()); err != nil { + t.Errorf("failed to create schema resources: %v", err) + t.FailNow() + } + + username := "dude" + email := "dude@b.cc" + ctx := getCtx() + + usernameFound, err := UsernameExists(ctx, db, username) + if err != nil { + t.Errorf("error checking for username {%s} existence: %q", + username, + err, + ) + } + + if usernameFound { + t.Errorf("unexpected: user{%s} should not have been found", + username, + ) + } + + if _, err := EmailExists(ctx, db, email); err != nil { + t.Errorf("unexpected: user email '%s' should not have been found", + email, + ) + } + + usr, err := CreateUser(ctx, db, email, username, "so strong") + if err != nil { + t.Errorf("failed to create user, error: %q", err) + t.FailNow() + } else if usr == nil { + t.Error("got nil usr back") + t.FailNow() + } + + if usr.Username != username { + t.Errorf("got back wrong username, want: %s, got: %s", + username, usr.Username, + ) + } + // ...more checks... + } + \end{verbatim} + \end{varwidth} + \caption{Example integration test} + \label{fig:integrationtest} +\end{figure} \n{2}{Click-ops}