Рассмотрим переход на другую страницу с помощью sendRedirect()
sendRedirect() – перенаправляет на любой указанный аргументом адрес.
Адрес в адресной строке браузера клиента меняется на тот, что указан аргументом у метода, так как сервер указывает клиенту, что нужно перейти по этому адресу и браузер переходит по нему.
Пример программы:
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.annotation.*;
@WebServlet(“/redirservlet”)
public class MyServlet extends HttpServlet{
protected void doGet(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException{
//сервер указывает браузеру клиента перейти по ссылке
resp.sendRedirect(“https://google.com”);
}
}
Скомпилируем файл и в адресной строке перейдем по адрусу /redirservlet.
Как видим сервлет сказал браузеру клиента перейти по ссылке https://google.com и он перешел.
Переход на другую страницу с помощью forward()
forward() – вызывает любой другой ресурс (другой сервлет, jsp) на этом же сервере.
Адресная строка браузера клиента при таком переходе не меняется. У клиента может поменяться страница, но клиент понятия не имеет куда его перенаправилов плане адреса, так как перенаправление произошло на сервере.
Также этот метод полезен тем, что аргументами ему передаются req и resp ресурса, из которого происходит перенаправлениеforward(req, resp).
То есть между сервлетами сервера можно передавать одни и те жеreqиresp.
Также forward понятное дело быстрее чем sendRedirect, так как он не передает никаких адресов клиенту чтобы он по ним переходил.
Все переходы происходят на сервере.
Давайте создадим еще один простенький сервлет MyServlet1.java, к которому будет происходить переход.
Этот сервлет выглядит так:
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.RequestDispatcher;
import javax.servlet.annotation.*;
@WebServlet(“/forwardservlet”)
public class MyServlet extends HttpServlet{
protected void doGet(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException{
//В getRequestDispatcher передаем адрес сервлета
//на этом сервере куда будет происходить переход
//из текущего сервлета.
RequestDispatcher rd = req.getRequestDispatcher(“/myservlet”);
//передаем в сервлет myservlet req и resp
//текущего сервлета и вызываем его.
rd.forward(req, resp);
}
}
Сервлет же, из которого будет совершаться переход, выглядит так:
Скомпилируем файлы сервлетов и в адресной строке перейдем по адрусу /forwardservlet.
Как видим, клиенту был отправлен текст Hello World!!!. И передал его клиенту очевидно сервлет MyServlet1.
Браузер клиента при этом не знает, что его куда-то перенаправили и даже адресная строка, как уже было сказано, не меняется (forwardservlet не измениться на myservlet).
Включение другой страницы в текущую с помощью include()
include – буквально просто включение кода другого сервлета на сервере в текущий сервлетс передачей req и resp текущего сервлета во включаемый сервлет.
Код сервлета MyServlet, в который будет включаться MyServlet1:
GET-запросы позволяют клиенту передавать параметры через адресную строку браузера. Эти параметры затем можно легко извлечь из объекта HttpServletRequest в сервлете.
Представьте, что клиент вводит в адресной строке следующее:
Здесь param1 имеет значение “Hello”, а param2 — “World”.
С помощью различных методов эти параметры можно перехватывать, например, с помощью метода getParameter получим значения параметров и запишем в строки.
Пример программы:
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.annotation.*;
@WebServlet(“/getpostservlet”)
public class MyServlet extends HttpServlet{
protected void doGet(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException {
//С помощью различных методов эти параметры
//можно перехватывать например с помощью
//метода getParameter получим значения
//параметров и запишем в строки.
String par1 = req.getParameter(“param1”);
String par2 = req.getParameter(“param2”);
PrintWriter pw = resp.getWriter();
//На странице клиента выведутся
//значения параметров.
pw.println(par1);
pw.println(par2);
//Также есть другие методы для удобства работы
//с параметрами например getParameterValues,
//getParameterNames ….
//Также мы можем получить некоторую информацию
//о запросе клиента с помощью методов ниже.
//например полный URL.
pw.println(req.getRequestURL());
//или получим IP хоста
pw.println(req.getRemoteHost());
}
}
Скомпилируем файл и в адресной строке передадим параметры сервлету по адресу /getpostservlet. Он должен отправить их обратно клиенту в браузер.
Как видим, сервлет успешно вернул клиенту параметры. Также сервлет отправил клиенту адрес и ip хоста, как видим.
Get запрос с помощью формы
Пусть сервлет отправляет форму с двумя полями и кнопкой отправки пользователю в браузер.
Если он в нее что-то введет то данные отправятся в наш сервлет по методу GET (в форме ниже можно увидеть method=’get’).
И таким образом когда пользователь введет данные в форму и нажмет кнопку для отправки данных формы, то в адресной строке появиться идентичная строка той, что была в предыдущем примере когда мы вводили параметры в ссылку вручную, только значения параметров в адресной строке будут взяты из текстовых полей формы:
localhost:8080\helloservlet\getpostservlet\?param1=(то, что ввел в текстовое поле пользователь)¶m2=(то, что ввел в текстовое поле пользователь)
Пример программы:
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.annotation.*;
@WebServlet(“/getpostservlet”)
public class MyServlet extends HttpServlet{
protected void doGet(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException {
String par1 = req.getParameter(“param1”);
String par2 = req.getParameter(“param2”);
PrintWriter pw = resp.getWriter();
//Отправляем клиенту, который обращается
//к этому сервлету форму чтобы он мог ввести
//туда параметры в адресной строке.
pw.println(“” +
“” +
“” +
“” +
“” +
““);
//Отправим присланные параметры обратно клиенту
pw.println(par1);
pw.println(par2);
}
}
Скомпилируем файл сервлета, перейдем по адресу /getpostservlet. Как видим, сервлет отправил клиенту форму и он теперь может вводить в поля параметры param1 и param2.
Когда пользователь нажимает кнопку, введенные им значения параметров param1 и param2отправляются в сервлет.
Как видим, в адресной строке URL идентичен тому, что мы вводили руками в предидущем примере, только здесь мы использовали форму для отправки параметров.
Также видим Hello World. То есть сервлет вернул клиенту параметры, которые клиент отправил в сервлет через форму.
Post запрос с помощью формы
Но часто на безопасно передавать параметры методом get, так как, как мы видели, они выводятся в адресной строке.
Чтобы они не выводились в адресной строке следует передавать параметры из формы методом POST.
Пример программы:
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.annotation.*;
@WebServlet(“/getpostservlet”)
public class MyServlet extends HttpServlet {
// Когда пользователь обращается к сервлету по адресу
// http://localhost:8080/helloservlet/getpostservlet
// он совершает GET-запрос. Поэтому отправлять форму
// клиенту будем в doGet.
protected void doGet(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException {
PrintWriter pw = resp.getWriter();
// Отправляем клиенту, который обращается
// к этому сервлету, форму, чтобы он мог ввести
// нужные параметры в адресной строке.
// Но в этот раз эта форма отправляет POST-запрос.
pw.println(“” +
“” +
“” +
“” +
“” +
““);
}
// А форма, как можно увидеть выше, отправляет POST-запрос,
// поэтому принимать параметры и отправлять их обратно
// клиенту будем в doPost.
protected void doPost(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException {
String par1 = req.getParameter(“param1”);
String par2 = req.getParameter(“param2”);
PrintWriter pw = resp.getWriter();
// На странице клиента выведутся значения
// параметров, при этом у клиента в адресной
// строке ничего не будет.
pw.println(par1);
pw.println(par2);
}
}
Скомпилируем файл сервлета, перейдем по адресу /getpostservlet. Как видим, сервлет отправил клиенту форму и он теперь может вводить в поля параметры param1 и param2.
Когда пользователь нажимает кнопку, введенные им значения параметров param1 и param2отправляются в сервлет.
Видим, что сервлет вернул клиенту параметры, которые он отправлял в сервлет.
Также видим самое главное, что параметров в адресной строке нет, они скрыты благодаря методу Post.
Рассмотрим вкратце зачем нужны методы init, doPost, service и destroy.
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyServlet extends HttpServlet {
/*
public void init() throws ServletException {
// При создании объекта сервлета, который создается один раз
// за все время работы сервера вызывается метод init(), в
// котором можно инициализировать какие-то данные до того
// как сервлет начнет их обрабатывать.
}
*/
protected void doGet(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException {
// Этот метод для приема Get запросов.
// Получаем поток PrintWriter с которым мы уже знакомы из resp
// через который будем отправлять данные клиенту.
PrintWriter pw = resp.getWriter();
pw.write(“Hello World!!!”);
}
/*
protected void doPost(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException {
// Этот метод для приема POST запросов.
// Ниже пример того как мы можем вызывать
// и GET запрос при POST запросе вызывая верхний метод
// и передав ему параметры из этого метода doPost.
doGet(req, resp);
}
public void service(ServletRequest req, ServletResponse resp)
throws ServletException, IOException {
// Каждый раз когда пользователь отправляет что-либо из браузера
// вызывается метод service(), который определяет вид запроса,
// который передал пользователь (GET, POST, PUT, DELETE)
// и вызывает соответствующий метод (doGet, doPost,
// doPut, doDestroy) метод service автоматически вызывает
// методы doGet, doPost…, поэтому нет необходимости переопределять
// его нет кроме редких случаев когда нам нужно чтобы все
// возможные запросы обрабатывались в одном методе
// гость в методе service так как этот метод
// перехватывает все возможные запросы.
}
public void destroy() throws ServletException {
// при удалении объекта сервлета можно очистить какие-то ресурсы.
// например закрыть подключение к базе
}
*/
}
Класс сервлета наследуется от классаHttpServlet и в нем реализуются методы doGet, doPost.
В этих методах происходит прием информации от клиента через HttpServletRequest req и отправка информации клиенту через HttpServletResponse resp.
Пример программы:
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyServlet extends HttpServlet {
//Этот метод для приема Get запросов.
//Что такое Get, Post запросы разберемся позже.
protected void doGet(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException {
//Получаем поток PrintWriter с которым мы уже знакомы
//из resp через который будем отправлять данные клиенту.
PrintWriter pw = resp.getWriter();
pw.write(“Hello World!!!”);
}
}
Чтобы запустить сервлет его нужно сначала скомпилировать используя библиотеку в папке lib servlet-api.jar.
Перейдем в консоли в папку apache-tomcat-9.0.80\webapps\helloservlet\WEB-INF\classes и здесь скомпилируем MyServlet.java с использованием servlet-api.jar:
В папке как обычно появиться файлик с байткодом .class.
Конфигурация Сервлета с помощью web.xml
В файле Web.xml настраивается то, как будет развернут наш сервлет MyServlet.java.
Например, по какому адресу клиенты будут отправлять запросы для обработки нашим сервлетом и многие другие настройки.
Web.xml:
MyServlet/myservletMyServletMyServlet5
Теперь можно проверять работу нашего сервлета.
Проверка работы сервлета
Запускаем файл startup.bat в папке bin для запуска контейнера сервлетов, то есть сервера.
Ждем пока полностью запуститься наш Tomcat.
Теперь через браузер сделаем запрос к нашему сервлету по аддресу, который мы прописывали в web.xml.
Всё работает. Как видим, мы как клиент через браузер отправили запрос серверу по адресу http://localhost:8080/helloservlet/myservlet и он отправил в ответ Hello World!!!.
Java EE – стандарт разработки веб-приложенийиспользуя язык Java.
Предоставляет множество средств для реализации клиент-серверных приложений.
В Java EE входят разные технологии – Servlets, JSP, JSTL и т.д.
В данном разделе будем рассматривать Сервлеты.
Что такое Сервлет?
Для начала нужно узнать что такое клиент-серверное приложение.
Говоря просто, механизм его работы такой:
Запросы с клиентского компьютера посылаются на серверный компьютер, на котором храниться и запускается код, и этот код обрабатывает запросы клиента, которые приходят на сервер.
Сервлет – это и есть этот код, который обрабатывает эти запросы.
Контейнер сервлета
Сервер(также называют серверная программа или контейнер сервлета) – это то, что содержит в себе сервлет.
Сервер работает без остановки, чтобы постоянно принимать запросы и давать ответ.
Контейнер сервлета обеспечивает функционирование сервлета (управление его жизненным циклом).
Жизненный цикл сервлета таков:
При запуске контейнера сервлета он загружает в себя сервлет, создает его объект и вызывает у него метод init(), и теперь сервлет готов к обработке запросов
Далее контейнер сервлетабудет передавать запросы пользователей в метод service сервлета. Каждый запрос пользователя обрабатывается в отдельном потоке.
При завершении работы контейнера сервлета останавливается и сервлет, путем вызова на нем метода destroy.
Есть много контейнеров сервлетов, например, Tomcat (далее будем пользоваться им).
Tomcat скачать в интернете довольно просто. Переходим по ссылке и скачиваем:
Логирование настраивается в файле log4j.properies.
Логгер записывает данные через appender. Аппендер бывает разных типов – для записи логов в файл или консоль, или бд, или другое.
Логирование происходит исходя из настроенного уровня логирования.
Их шесть: trace < debug < info < warn < error < fatal.
По стрелке слева на право сужается количество информации, которая будет логироваться.
При например настроенном trace логироваться может вообще всё, то есть все уровни.
При, например, errorтолько ошибки, то есть error и fatal уровни.
Если настроен уровень info, срабатывать будут только логи уровней info,warn,error,fatal – все выше уровнем.
Также есть layout-ы, они определяют то, КАК будет выводиться информация в лог.
Все эти настройки совершаются в log4j.properties.
Настройка логирования в консоль
Приведем пример настройки логирования в консоль.
Здесь мы настраиваем уровень логирования, аппендер и лейаут.
Файл же этот пусть находиться в той же папке, что и программа, которая будет логировать.
Содержимое конфигурационного файла:
#здесь пишем уровень логирования и используемые в данном конфигурационном файле аппендеры
log4j.rootLogger=INFO, consoleAppender
#Appender №1
#consoleAppender – логироваться будет в консоль
log4j.appender.consoleAppender=org.apache.log4j.ConsoleAppender
#PatternLayout определяет какие данные будут логироваться и как будут,
log4j.appender.consoleAppender.layout=org.apache.log4j.PatternLayout
#например через ConversionPattern с помощью %d задается
#формат даты. или %p уровень лога и т.д. То есть в соответствии
#с паттерном ниже логироваться данные в консоль будут еще и с указанием даты
#логирования информации, уровня логирования данной информации и другое.
log4j.appender.consoleAppender.layout.ConversionPattern=%d{yyyy-MM-dd}-%t-%p-%10c:%m%n
Уже был показан метод errorдля логирования ошибок.
Есть также методы для логирования других уровней:
Метод info – для логирования какой-либо информации по ходу программы (не ошибок, просто какие-то данные), debug – для логирования во время дебаггинга программы и т.д., название этих методов говорит само за себя.
Пример программы:
import java.util.*;
import java.sql.*;
import org.apache.log4j.Logger;
public class Logging {
private static Logger log = Logger.getLogger(Logging.class);
public static void main(String[] args) {
//с помощью методов debug(), info(), … мы указываем
//уровень информации, которая логируется и в них передаем
//информацию, которая логируется.
log.debug(“This is debug method”);
//в properties уровень указан info поэтому
//уровень debug log не сработает.
//То есть мы можем отключать с помощью настроек
//некоторые методы для логирования.
//То есть если программист много раз по ходу программы
//использовал метод debug для дебагинга программы
//и если он закончил дебаг
//и ему сейчас больше не нужны
//вызовы этого метода в программе
//он может не стирать их из программы, а просто
//их отключить чтобы они больше ничего не логировали.
log.info(“This is info method”);
log.error(“This is error method”);
}
}
Скомпилируем и запустим.
Видим в консоль залогировались только данные из методов info и error по вышеизложенным причинам.
Также видим, что перед данными, которые логировались также указаны дополнительные данные, которые мы настраивали с помощью layout.ConversionPattern.
Настройка логирования в файл
Давайте сделаем так, чтобы данные логировались не только в консоль, а еще и в файл. Для этого будем использовать другой аппендер.
Содержимое конфигурационного файла:
#здесь пишем уровень логирования и используемые в данном конфигурационном файле аппендеры
log4j.rootLogger=INFO, consoleAppender, fileAppender
#Appender №1
#consoleAppender – логироваться будет в консоль
log4j.appender.consoleAppender=org.apache.log4j.ConsoleAppender
#PatternLayout определяет какие данные будут логироваться и как будут,
log4j.appender.consoleAppender.layout=org.apache.log4j.PatternLayout
#например через ConversionPattern с помощью %d задается
#формат даты. или %p уровень лога и т.д. То есть в соответствии
#с паттерном ниже логироваться данные лога будут еще и с указанием даты
#логирования информации, уровня логирования данной информации и другое.
log4j.appender.consoleAppender.layout.ConversionPattern=%d{YYYY-MM-dd}-%t-%p-%l-10c:%m%n
#Appender №2
#FileAppender – лог в файл. RollingFileAppender дает возможность задать макс.
#размер файла лога и при достижении этого размера
#создаться новый файл лога и так по кругу
log4j.appender.fileAppender=org.apache.log4j.RollingFileAppender
log4j.appender.fileAppender.layout=org.apache.log4j.PatternLayout
#также как и в прошлом аппендере
log4j.appender.fileAppender.layout.ConversionPattern=%d{yyyy-MM-dd}-%t-%p-%l-10c:%m%n
#файл в который будет логироваться
log4j.appender.fileAppender.File=demoApplication.log
Пример программы:
import java.util.*;
import java.sql.*;
import org.apache.log4j.Logger;
public class Logging {
private static Logger log = Logger.getLogger(Logging.class);
public static void main(String[] args) {
log.debug(“This is debug method”);
log.info(“This is info method”);
log.error(“This is error method”);
}
}
Скомпилируем и запустим.
В этом случае данные залогировались теперь не только в консоль, а и в файл, который автоматически создается в папке где находиться файл программы.
Настройка логирования в html файл
Также можно логировать в html файл, он будет оформлен в формате таблицы. Или можно логировать в xml файл. Логирование происходит, опять же, с помощью fileAppender.
Содержимое конфигурационного файла:
#здесь пишем уровень логирования и используемые в данном конфигурационном файле аппендеры
log4j.rootLogger=INFO, consoleAppender, fileAppender
#Appender №1
#consoleAppender – логироваться будет в консоль
log4j.appender.consoleAppender=org.apache.log4j.ConsoleAppender
#PatternLayout определяет какие данные будут логироваться и как будут,
log4j.appender.consoleAppender.layout=org.apache.log4j.PatternLayout
#например через ConversionPattern с помощью %d задается
#формат даты. или %p уровень лога и т.д. То есть в соответствии
#с паттерном ниже логироваться данные будут еще и с указанием даты
#логирования информации, уровня логирования данной информации и другое.
log4j.appender.consoleAppender.layout.ConversionPattern=%d{YYYY-MM-dd}-%t-%p-%10c:%m%n
#Appender №3
log4j.appender.fileAppender=org.apache.log4j.FileAppender
log4j.appender.fileAppender.File=htmllayout.html
#HTMLLayout – можем сохранять логи в виде HTML страницы.
#есть еще XMLLayout тоже самое только в виде XML.
log4j.appender.fileAppender.layout=org.apache.log4j.HTMLLayout
#можно установить заголовок на HTML страницы
log4j.appender.HTMLLayout.Title=HTML Layout Example
#будет отображаться информация о локации, времени и др.
log4j.appender.fileAppender.LocationInfo=true
Пример программы:
import java.util.*;
import java.sql.*;
import org.apache.log4j.Logger;
public class Logging {
private static Logger log = Logger.getLogger(Logging.class);
public static void main(String[] args) {
log.debug(“This is debug method”);
log.info(“This is info method”);
log.error(“This is error method”);
}
}
Скомпилируем и запустим.
В этом случае данные залогировались теперь не только в консоль, а и в html файл, который автоматически создается в папке где находиться файл программы.
Часто бывают ситуации когда нам нужно записать куда-то данные о работе программы или об произошедших ошибках в ней чтобы потом их просмотреть.
Этим занимается логгер.
Приведем простейший пример программы, в которой логируется информация о произошедшей в программе ошибке в файл, чтобы программист в любое время мог открыть этот файл и посмотреть, что же произошло.
Чаще всего приходиться логировать информацию в блоке catch о произошедшей ошибке в блоке try.
Пример программы:
import java.util.*;
import java.sql.*;
import org.apache.log4j.Logger;
public class Logging {
private static Logger log = Logger.getLogger(Logging.class);
public static void main(String[] args) {
try {
//Видим что ниже мы указали не правильные
//данные БД поэтому произойдет ошибка
//и потом она залогируется в блоке catch.
Connection connection =
DriverManager.getConnection(
“jdbc:mysql://localhost/dfgwefwqwq”,
“root”, “07OMSDDD”);
} catch(SQLException e){
//вот таким образом обычно логируется ошибка.
//запись произойдет в файл.
log.error(e.getMessage());
}
}
}
Скомпилируем и запустим программу с указанием файла log4j.properties, в котором настраивается куда происходит логирование и как будет логироваться информация. Настройку логирования в этом файле разберем в следующих уроках:
В файл в результате записалась информация о произошедшей ошибке.
Mockito довольно нужная штука и к сожалению немного сложноватая для понимания, поэтому здесь подробно.
Часто бывает так, что подлежащий тестированию метод имеет какие-то зависимости. Например, в нем производиться подключение к базе данных либо в тестируемом методе используются классы или методы со сложной структурой. Это делает наш тестируемый метод сложно поддерживаемым, то есть если перенести его на другой компьютер, на котором нет базы от которой зависит метод или каких либо классов, которые используются в этом методе, то без этих зависимостей метод протестировать не получиться.
Было бы весьма наивно и не практично создавать новую базу и прописывать sql запросы к ней и т.д. чтобы протестировать этот метод на этом другом компьютере, ведь можно просто заменить зависимости в тестируемом методе на то, что эти зависимости могли бы вернуть.
Всё, что нам нужно – это протестировать сам конкретный метод, а корректность работы зависимостей в нем можно просто сымитировать.
mock и spy. Разница
Для имитации зависимостей есть mock, spy и stub.
Разница в mock и spy будет понятна когда дойдем до stub, а пока просто попробуем понять их суть:
mockсоздает фейковый экземпляр какого-то класса, который является зависимостью в тестируемом методе и все методы и поля этого фейкового экземпляра возвращают null или 0 или false в зависимости от типов переменных или типов возвращаемых значений методов.
Благодаря этому мы сможем протестировать метод, который мы хотим, используя в нем фейковые методы фейкового экземпляра. Экземпляр-зависимость метод которого, например, должен возвращать данные из БД в тестируемом методе, ясное дело, не будет возвращать данные из БД поскольку будет использован метод фейкового экземпляра зависимости и этот метод вернет пустоту. Зато хоть можем протестировать.
spy же в отличие от mockне создает никаких экземпляров, он работает с существующим экземпляром-зависимостью и с содержимым этого существующего экземпляра. То есть с его полями и методами просто по факту применения к нему spy ничего не происходит.
И здесь подходим к stub. с помощью стаба мы можем подменить то, что возвращают нам методы mock и spy экземпляров на то, что мы хотим.
stubэто вот такая конструкция:
when (вызывается метод mock или spy экземпляра).thenReturn(вот такие значения, которые мы хотим).
То есть в скобочках у when указывается метод из mock или spy экземпляра, возвращаемые значения которого, будут подменяться при его вызовах в тестируемом методе на те значения, которые указываются в скобочках у thenReturn.
stub может использоваться, как в mock, так и в spy экземпляре.
Очевидно пустой mock экземпляр становиться уже не таким пустым поскольку с помощью стаба некоторые методы в нем будут возвращать уже не пустоту, а поддельные значения, а spy же это буквально шпион (spy с англ. шпион) он забирается в нормальный экземпляр (то есть экземпляр не пустой) и заменяет возвращаемые значения некоторых методов в нем.
mock
Давайте для начала создадим класс, который будет извлекать данные из базы данных с помощью средств JDBC.
Пример программы:
import java.util.*;
import java.sql.*;
public class DbClass {
//В этой функции просто извлекаем данные
//о коммутаторах из базы, запихиваем их
//в список и возвращаем этот список.
public List< String > getCommutaorsDataFromDB() {
List< String > commutatorsDataFromDB =
new ArrayList< String >();
try {
Connection connection =
DriverManager.getConnection(
“jdbc:mysql://localhost/storage”,
“root”, “07031998MSD”);
Statement statement =
connection.createStatement();
ResultSet resultSet =
statement.executeQuery(
“SELECT * FROM commutator”);
while (resultSet.next()) {
commutatorsDataFromDB.add(
resultSet.toString());
}
} catch(SQLException e){}
return commutatorsDataFromDB;
}
//это метод возведения в степень он здесь
//просто чтобы показать разницу spy и mock
public int pow(int num, int power){
int result=1;
for(int i=0; i < power; i++){result*=num;}
return result;
}
}
Этот класс вместе с методом getCommutatorsDataFromDB и будет зависимостью, которую мы будем подменять в методе, который мы будем тестировать.
Теперь давайте создадим класс в котором будет метод, который мы будем тестировать. В этом методе, как раз, будет использоваться зависимость. То есть метод getCommutatorsDataFromDB, который возвращает данные из БД. Но этой БД, как мы помним, нетна данном компьютере. Поэтому нам придется подменять возвращаемые значения этой зависимости.
Пример программы:
import java.util.*;
public class MockExample {
private DbClass dbClass;
MockExample(DbClass dbClass){
this.dbClass = dbClass;
}
//Этот метод будем тестировать.
//В этой функции вызывается getCommutaorsDataFromDB.
//то есть TestMethod имеет зависимость on dbClass
//то есть от БД, к которой представим что у нас
//нет доступа на текущем компьтере. Значит
//возвращаемые значения метода getCommutaorsDataFromDB
//нужно будет подменить
//то есть в dbClass объект, который здесь либо будем
//отправлять шпиона spy, либо будем делать фейковый
//объект dbClass с помощью mock меняя
//в нем getCommutaorsDataFromDB.
public List< String > getDbDataForTestWithPlusses() {
List< String > commutatorsDataFromDB =
dbClass.getCommutaorsDataFromDB();
//Пусть этот метод будет добавлять плюс в конец
//названия каждого коммутатора в списке коммутаторов,
//который мы извлекли из из БД и этот обновленный список
//с плюсами будет возвращать этот метод.
for(int i=0; i < commutatorsDataFromDB.size(); i++){
commutatorsDataFromDB.set(
i, commutatorsDataFromDB.get(i)+"+" );
}
return commutatorsDataFromDB;
}
}
Теперь создаем класс тестер в котором создаем тестовый метод, который будет тестировать метод с подмененной зависимостью.
Пример программы:
import org.junit.Assert;
import org.junit.Test;
import java.util.*;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class TestMock {
//Создаем фейковый объект класса DbClass
//с пустым содержимым.
//Будет использоваться в тестируемом методе
//с подмененным методом getCommutaotrsDataFromDB.
DbClass mockDbClass = mock(DbClass.class);
@Test
public void TestMethod() throws Exception {
//Передаем фейковый объект класса DbClass
//в объект класса MockExample. Там он будет
//использоваться вместо настоящего объекта-зависимости,
//который использовался бы
//для извлечения коммутаторов из БД.
MockExample mockExample = new MockExample(mockDbClass);
//Вместо того что должен выводить
//метод getCommutaotrsDataFromDB()
//получить данные из БД в виде списка,
//будут выводить список ниже.
List< String > fakeCommutaotrsDataFromDB =
Arrays.asList(“comm1”, “comm2”, “comm3”);
List< String > fakeCommutatorsDataFromDB =
Arrays.asList(“comm1”, “comm2”, “comm3”);
//Ниже мы говорим что когда вызывается метод
//getCommutatorsDataFromDB у объекта mockDbClass
//то выводится список fakeCommutatorsDataFromDB
//вместо настоящего списка коммутаторов из БД.
//Метод getCommutatorsDataFromDB в фейковом объекте
//mockDbClass ясное дело будет использоваться
//в методе getDbDataForTestWithPlusses объекта
//mockExample.
when(mockDbClass.getCommutatorsDataFromDB()).thenReturn(
fakeCommutatorsDataFromDB);
//Метод getCommutatorsDataFromDB в фейковом
//объекте mockDbClass мы подменили.
//Все другие методы в этом классе все еще
//должны возвращать либо пустую либо ноль,
//либо false. Проверим метод pow.
System.out.println(mockDbClass.pow(4,2));
//Теперь тестируем метод getDbDataForTestWithPlusses
//в объекте mockExample с фейковой зависимостью в нем
//с подмененным методом getCommutatorsDataFromDB.
Assert.assertEquals(Arrays.asList(“comm1+”, “comm2+”,
“comm3+”), mockExample.getDbDataForTestWithPlusses());
}
}
Для того чтобы скомпилировать и запустить класс тестер, необходимо для начала скачать такие библиотеки:
junit-4.13.2.jar
hamcrest-core-1.3.jar
mockito-core-4.11.0.jar
byte-buddy-1.12.20.jar
byte-buddy-agent-1.12.20.jar
Всех их довольно легко найти и скачать в интернете, например на mvnrepository.com.
Итак компилируем и запускаем:
Видим, что сначала вывело 0, что значит, что объект mockDbClassтаки был фейковым, то есть пустым, так как его метод pow вернул 0, а не 16.
В то же время, очевидно, подмена метода getCommutatorsDataFromDB в этом объекте была успешна, так как программа говорит, что всё ОК, что значит, что фактическое значение совпало с ожидаемым. То есть тестируемый метод вернул имена коммутаторов с добавленным плюсом в конце, как и нужно было.
Еще пару слов, чтобы вы окончательно уловили суть. Мы проверяли функционал тестируемого метода. Функционалом тестируемого метода было добавление плюсов к значениям в списке. При этом нам было абсолютно не важно, что за значения в этом списке. И именно потому, что нам было абсолютно не важно, что за значения в этом списке, мы и добавили туда три любых значения – “comm1”, “comm2”, “comm3” и на них тестировали функционал тестируемого метода.
spy
Теперь создадим такой же класс тестер только уже со spy вместо mock.
Пример программы:
import org.junit.Assert;
import org.junit.Test;
import java.util.*;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
public class TestMock {
// используя spy создаётся
// нормальный объект класса DbClass
// уже не фейковый то есть не пустой,
// но в нем может быть подменённый метод.
// Будет использоваться в тестируемом методе
// с подменённым методом getCommutaorsDataFromDB.
DbClass spyDbClass = spy(DbClass.class);
@Test
public void TestMethod() throws Exception {
// Передаем spy объект класса DbClass
// в котором будет подменённый метод
// в объект класса MockExample.
MockExample mockExample = new MockExample(spyDbClass);
// Вместо того что должен выводить
// метод getCommutaorsDataFromDB()
// покажет данные из БД в виде списка,
// будет выводиться список ниже.
List< String > fakeCommutaorsDataFromDB =
Arrays.asList(“comm1”, “comm2”, “comm3”);
// Ниже мы показываем что когда вызывается метод
// getCommutaorsDataFromDB у объекта spyDbClass
// то выводится список fakeCommutaorsDataFromDB
// вместо реальных коммутаторов из БД.
// Метод getCommuttatorsDataFromDB из spy объекта
// spyDbClass ясное дело будет использоваться
// в методе getDbDataForTestWithPlusses объекта
// mockExample.
when(spyDbClass.getCommuttatorsDataFromDB()).thenReturn(fakeCommutaorsDataFromDB);
// Метод getCommuttatorsDataFromDB в spy
// объекте spyDbClass мы подменили.
// Все другие методы в этом объекте
// должны работать нормально, они не должны
// возвращать пустоту как в случае с mock.
// spyDbClass это самый обычный объект но с
// подмененным методом getCommuttatorsDataFromDB.
// Метод pow очевидно должен возвращать 16.
System.out.println(spyDbClass.pow(4,2));
// Теперь тестируем метод getDbDataForTestWithPlusses
// В объекте mockExample со spy зависимостью в нем
// с подмененным методом getCommuttatorsDataFromDB.
Assert.assertEquals(Arrays.asList(“comm1”, “comm2”, “comm3”),
mockExample.getDbDataForTestWithPlusses());
}
}
Итак, компилируем и запускаем:
Видим, что сначала вывело 16. Это то, что вернул метод pow, что значит что методы в spy объекте работают нормально, они не возвращают пустоту как mock объект.
Также очевидно, что методgetCommutatorsDataFromDB был подменен успешно, так как тестирование прошло успешно.
В скобках у аннотации Test можно указать число. Это число – это количество миллисекунд. Если метод будет выполняться больше этого количества миллисекунд, то программа сообщит об этом.
Таким образом можно протестировать производительность тестируемого метода.
Пример программы:
import org.junit.Assert;
import org.junit.Test;
public class TestCalc {
// проверяет, что метод исполняется
// не более 100 миллисекунд
@Test(timeout = 100)
public void testPow2() throws Exception {
Calc calc = new Calc();
int result = 0;
// Сделаем так чтобы тестовый
// метод длился более 100 миллисекунд.
// Хотя этот цикл было бы логичнее разместить
// в тестируемом методе pow, так как мы тестируем его
// производительность, а не тестового метода.
for (int i = 0; i < 1000000; i++) {
result = calc.pow(2, 4);
}
Assert.assertEquals(16, result);
}
}
Скомпилируем и запустим:
Видим, что программа сообщает нам о том, что метод выполнялся более 100 миллисекунд. Значит очевидно нужно налаживать производительность.
Аннотация @Ignore
Если в тестовом классе несколько тестовых методов, то мы можем отключать какие-либо из них с помощью аннотации Ignore
Пример программы:
import org.junit.Assert;
import org.junit.Test;
import org.junit.Ignore;
public class TestCalc {
@Test
public void testPow() throws Exception{
Calc calc = new Calc();
int result = calc.pow(2, 4);
Assert.assertEquals(14, result);
}
//Также какой-либо тест можно отключить
//(следующий тест за этой
//аннотацией не запуститься).
@Ignore
@Test
public void testPow1() throws Exception{
Calc calc = new Calc();
int result = calc.pow(2, 4);
Assert.assertEquals(16, result);
}
@Test(timeout=100)
public void testPow2() throws Exception{
Calc calc = new Calc();
int result = 0;
for(int i=0;i<1000000;i++){
result = calc.pow(2, 4);
}
Assert.assertEquals(16, result);
}
}
Компилируем и запускаем:
Как видим, хоть тестовых метода у нас три, а тестировалось всего два – Tests run: 2. Так как второй тестовый метод мы отключили.
Аннотации @BeforeClass, @AfterClass
С помощью аннотации @BeforeClassможно создать метод, который будет выполнен единожды перед тем, как начнут выполняться тестовые методы, а с помощью аннотации @AfterClassможно создать метод, который будет выполнен единожды после того, как выполняться все тестовые методы.
Зачем же нужны эти методы?
В случае с BeforeClass, чтобы, например, инициализировать какие-то данные, которые будут использоваться во всех тестах, в случае с AfterClass освободить какие-то ресурсы.
Пример программы:
import org.junit.Assert;
import org.junit.Test;
import org.junit.BeforeClass;
import org.junit.AfterClass;
public class TestCalc {
//этот метод запускается перед тестовыми
@BeforeClass
public static void testBeforeClass() throws Exception{
System.out.println(“before all classes”);
}
//этот метод запускается после всех тестовых
@AfterClass
public static void testAfterClass() throws Exception{
System.out.println(“after all classes”);
}
@Test
public void testPow() throws Exception{
System.out.println(“Test method 1”);
Calc calc = new Calc();
int result = calc.pow(2, 4);
Assert.assertEquals(14, result);
}
@Test
public void testPow1() throws Exception{
System.out.println(“Test method 2”);
Calc calc = new Calc();
int result = calc.pow(2, 4);
Assert.assertEquals(16, result);
}
@Test(timeout=100)
public void testPow2() throws Exception{
System.out.println(“Test method 3”);
Calc calc = new Calc();
int result = calc.pow(2, 4);
Assert.assertEquals(16, result);
}
}
Компилируем и запускаем.
Видим, что сразу вывелось сообщение из метода помеченного@BeforeClass,потом сообщения тестовых методов, потом сообщение из метода помеченного@AfterClass.
Аннотации @Before, @After
С помощью аннотации @Before можно создать метод, который будет выполняться перед каждым тестовым методом, а с помощью аннотации @Afterможно создать метод, который будет выполняться после каждого тестового метода
Пример программы:
import org.junit.Assert;
import org.junit.Test;
import org.junit.Before;
import org.junit.After;
public class TestCalc {
//этот метод запускается перед каждым тестовым
@Before
public void testBefore() throws Exception{
System.out.println(“before every class”);
}
//этот метод запускается после каждого тестового
@After
public void testAfter() throws Exception{
System.out.println(“after every class”);
}
@Test
public void testPow() throws Exception{
System.out.println(“Test method 1”);
Calc calc = new Calc();
int result = calc.pow(2, 4);
Assert.assertEquals(14, result);
}
@Test
public void testPow1() throws Exception{
System.out.println(“Test method 2”);
Calc calc = new Calc();
int result = calc.pow(2, 4);
Assert.assertEquals(16, result);
}
@Test(timeout=100)
public void testPow2() throws Exception{
System.out.println(“Test method 3”);
Calc calc = new Calc();
int result = calc.pow(2, 4);
Assert.assertEquals(16, result);
}
}
Компилируем и запускаем.
Видим, что перед каждым сообщением из тестового метода выводилось сообщение из метода помеченного @Before, и также послекаждого сообщения тестового метода выводилось сообщение из метода помеченного аннотацией @After.