tex: add stuff on integration testing
This commit is contained in:
parent
6c2ccf4206
commit
81412b8898
143
tex/text.tex
143
tex/text.tex
@ -1150,12 +1150,143 @@ intended/expected.
|
|||||||
|
|
||||||
\n{2}{Integration tests}
|
\n{2}{Integration tests}
|
||||||
|
|
||||||
Integrating with external software, namely the database in case of this program
|
Integrating with external software, namely the database in case of this
|
||||||
is designed to utilise the same mechanism that was mentioned in the previous
|
program, is designed to utilise the same mechanism that was mentioned in the
|
||||||
section: Go's \texttt{testing} package. These test verify that the code changes
|
previous section: Go's \texttt{testing} package. These tests verify that the
|
||||||
can still perform the same actions with the external software that were
|
code changes can still perform the same actions with the external software that
|
||||||
possible before the change and are equally run before every commit locally and
|
were possible before the change and are run before every commit locally and
|
||||||
then in the CI.
|
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}
|
\n{2}{Click-ops}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user