├── .circleci
└── config.yml
├── .gitignore
├── README.md
├── img.png
├── pom.xml
└── src
├── main
└── java
│ └── com
│ └── ruubel
│ ├── Application.java
│ ├── aspect
│ └── AroundGetContactsAspect.java
│ ├── controller
│ └── BankController.java
│ ├── model
│ ├── Bank.java
│ └── BankInformation.java
│ └── service
│ ├── BankService.java
│ ├── factory
│ └── ScraperFactoryService.java
│ ├── observer
│ ├── BankInformationPublisherService.java
│ ├── BankInformationReceived.java
│ ├── PrinterService.java
│ └── SaverService.java
│ └── strategy
│ ├── BankScraperStrategy.java
│ ├── HttpFetchService.java
│ ├── SebScraper.java
│ └── SwedbankScraper.java
└── test
└── groovy
└── com
└── ruubel
├── aspect
└── AroundGetContactsAspectSpec.groovy
├── controller
└── BankControllerSpec.groovy
└── service
├── BankServiceSpec.groovy
├── factory
└── ScraperFactoryServiceSpec.groovy
├── observer
└── BankInformationPublisherServiceSpec.groovy
└── strategy
├── SebScraperSpec.groovy
└── SwebankScraperSpec.groovy
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Java Maven CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-java/ for more details
4 | #
5 | version: 2
6 | jobs:
7 | build:
8 | machine: true
9 | working_directory: ~/design-patterns-spring-boot
10 | environment:
11 | # Customize the JVM maximum heap limit
12 | MAVEN_OPTS: -Xmx3200m
13 | steps:
14 | - checkout
15 | - restore_cache:
16 | keys:
17 | - v1-dependencies-{{ checksum "pom.xml" }}
18 | # fallback to using the latest cache if no exact match is found
19 | - v1-dependencies-
20 | - run: mvn dependency:go-offline
21 | - save_cache:
22 | paths:
23 | - ~/.m2
24 | key: v1-dependencies-{{ checksum "pom.xml" }}
25 | # run tests!
26 | - run: mvn test
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | *.iml
3 | target/
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Design patterns in spring boot
2 | [](https://circleci.com/gh/indrekru/design-patterns-spring-boot)
3 |
4 | This repository is a simple spring boot application, that demonstrates a few design patterns:
5 |
6 | * Singleton
7 | * Controller
8 | * Factory
9 | * Strategy
10 | * Proxy
11 | * Observer
12 | * Aspect-oriented programming
13 |
14 | This demo application retrieves contact phone numbers from 2 different bank's websites (more banks can be added) with specific implementations per bank and offers a nice interface to hide the specifics.
15 |
16 | ## Getting Started
17 |
18 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See running for notes on how to run the project on a system.
19 |
20 | ### Prerequisites
21 |
22 | 1. Clone the project to your local environment:
23 | ```
24 | git clone https://github.com/indrekru/design-patterns-spring-boot.git
25 | ```
26 |
27 | 2. You need maven installed on your environment:
28 |
29 | #### Mac (homebrew):
30 |
31 | ```
32 | brew install maven
33 | ```
34 | #### Ubuntu:
35 | ```
36 | sudo apt-get install maven
37 | ```
38 |
39 | ### Installing
40 |
41 | Once you have maven installed on your environment, install the project dependencies via:
42 |
43 | ```
44 | mvn install
45 | ```
46 |
47 | ## Testing
48 |
49 | Run all tests:
50 | ```
51 | mvn test
52 | ```
53 |
54 | ## Running
55 |
56 | Once you have installed dependencies, this can be run from the `Application.java` main method directly,
57 | or from a command line:
58 | ```
59 | mvn spring-boot:run
60 | ```
61 |
62 | Open browser and go to http://localhost:8080/api/v1/banks and you should see the results
63 |
64 | ## Built With
65 |
66 | * [Spring Boot](https://spring.io/projects/spring-boot) - Spring Boot 2
67 | * [Spock](http://spockframework.org/) - Spock testing framework
68 | * [Maven](https://maven.apache.org/) - Dependency Management
69 |
70 | ## Contributing
71 |
72 | If you have any improvement suggestions please create a pull request and I'll review it.
73 |
74 |
75 | ## Authors
76 |
77 | * **Indrek Ruubel** - *Initial work* - [Github](https://github.com/indrekru)
78 |
79 | See also the list of [contributors](https://github.com/indrekru/design-patterns-spring-boot/graphs/contributors) who participated in this project.
80 |
81 | ## License
82 |
83 | This project is licensed under the MIT License
84 |
85 | ## Acknowledgments
86 |
87 | * Big thanks to Pivotal for Spring Boot framework, love it!
88 | * Also check out my Spring Boot 2 Oauth2 resource server example: https://github.com/indrekru/spring-boot-2-oauth2-resource-server
89 |
--------------------------------------------------------------------------------
/img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/indrekru/design-patterns-spring-boot/03a311478ca7644d486cf9d4270e878b559ba2a4/img.png
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | com.ruubel
8 | design-patterns-spring-boot
9 | 1.0-SNAPSHOT
10 |
11 |
12 | org.springframework.boot
13 | spring-boot-starter-parent
14 | 2.0.8.RELEASE
15 |
16 |
17 |
18 |
19 | org.springframework.boot
20 | spring-boot-starter-web
21 |
22 |
23 |
24 | org.springframework.boot
25 | spring-boot-starter-aop
26 |
27 |
28 |
29 | org.springframework.boot
30 | spring-boot-starter-test
31 | test
32 |
33 |
34 |
35 | org.spockframework
36 | spock-core
37 | 1.3-RC1-groovy-2.4
38 | test
39 |
40 |
41 |
42 | org.jsoup
43 | jsoup
44 | 1.11.3
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | org.springframework.boot
53 | spring-boot-maven-plugin
54 |
55 |
56 | org.codehaus.gmavenplus
57 | gmavenplus-plugin
58 | 1.6.2
59 |
60 |
61 |
62 | compileTests
63 |
64 |
65 |
66 |
67 |
68 | org.apache.maven.plugins
69 | maven-surefire-plugin
70 | 2.21.0
71 |
72 |
73 | **/*Spec.java
74 |
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/src/main/java/com/ruubel/Application.java:
--------------------------------------------------------------------------------
1 | package com.ruubel;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class Application {
8 |
9 | public static void main(String[] args) throws Exception {
10 | SpringApplication.run(Application.class, args);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/ruubel/aspect/AroundGetContactsAspect.java:
--------------------------------------------------------------------------------
1 | package com.ruubel.aspect;
2 |
3 | import org.aspectj.lang.ProceedingJoinPoint;
4 | import org.aspectj.lang.annotation.Around;
5 | import org.aspectj.lang.annotation.Aspect;
6 | import org.aspectj.lang.annotation.Pointcut;
7 | import org.springframework.stereotype.Component;
8 |
9 | import java.time.Duration;
10 | import java.time.LocalDateTime;
11 |
12 | /**
13 | * Created by indrek.ruubel on 03/07/2016.
14 | *
15 | * Proxy pattern:
16 | * The intent of this pattern is to provide a "Placeholder" for an object to control references to it.
17 | * https://www.oodesign.com/proxy-pattern.html
18 | *
19 | * Also we're using aspect-oriented programming here.
20 | * https://en.wikipedia.org/wiki/Aspect-oriented_programming
21 | */
22 | @Aspect
23 | @Component
24 | public class AroundGetContactsAspect {
25 |
26 | @Pointcut("execution(* com.ruubel.service.BankService.*(..))")
27 | public void serviceMethod() {}
28 |
29 | /**
30 | * In this case we wrap around the real method, controlling the input/output of that method, being a proxy.
31 | * As a simple example we measure method execution time.
32 | */
33 | @Around("serviceMethod()")
34 | public Object profile(ProceedingJoinPoint pjp) {
35 | LocalDateTime start = LocalDateTime.now();
36 | Object task = null;
37 | try {
38 | task = pjp.proceed();
39 | } catch (Throwable throwable) {
40 | throwable.printStackTrace();
41 | }
42 | LocalDateTime end = LocalDateTime.now();
43 | Duration duration = Duration.between(start, end);
44 | System.out.println("Execution took: " + duration.toMillis() + " ms");
45 | return task;
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/com/ruubel/controller/BankController.java:
--------------------------------------------------------------------------------
1 | package com.ruubel.controller;
2 |
3 | import com.ruubel.model.BankInformation;
4 | import com.ruubel.service.BankService;
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.http.HttpStatus;
7 | import org.springframework.http.ResponseEntity;
8 | import org.springframework.web.bind.annotation.GetMapping;
9 | import org.springframework.web.bind.annotation.RequestMapping;
10 | import org.springframework.web.bind.annotation.RestController;
11 |
12 | import java.util.List;
13 |
14 | /**
15 | * Created by indrek.ruubel on 02/07/2016.
16 | */
17 | @RestController
18 | @RequestMapping("/api/v1/banks")
19 | public class BankController {
20 |
21 | private BankService bankService;
22 |
23 | @Autowired
24 | public BankController(BankService bankService) {
25 | this.bankService = bankService;
26 | }
27 |
28 | @GetMapping
29 | private ResponseEntity banks() {
30 | List contacts = bankService.getContacts();
31 | return new ResponseEntity(contacts, HttpStatus.OK);
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/com/ruubel/model/Bank.java:
--------------------------------------------------------------------------------
1 | package com.ruubel.model;
2 |
3 | /**
4 | * Created by indrek.ruubel on 02/07/2016.
5 | */
6 | public enum Bank {
7 | SEB, SWEDBANK;
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/ruubel/model/BankInformation.java:
--------------------------------------------------------------------------------
1 | package com.ruubel.model;
2 |
3 | /**
4 | * Created by indrek.ruubel on 02/07/2016.
5 | */
6 | public class BankInformation {
7 |
8 | private Bank bank;
9 | private String phoneNumber;
10 |
11 | public BankInformation(Bank bank, String phoneNumber) {
12 | this.bank = bank;
13 | this.phoneNumber = phoneNumber;
14 | }
15 |
16 | public Bank getBank() {
17 | return bank;
18 | }
19 |
20 | public String getPhoneNumber() {
21 | return phoneNumber;
22 | }
23 |
24 | @Override
25 | public String toString() {
26 | return "BankInformation{" +
27 | "bank=" + bank +
28 | ", phoneNumber='" + phoneNumber + '\'' +
29 | '}';
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/com/ruubel/service/BankService.java:
--------------------------------------------------------------------------------
1 | package com.ruubel.service;
2 |
3 | import com.ruubel.model.BankInformation;
4 | import com.ruubel.service.factory.ScraperFactoryService;
5 | import com.ruubel.service.observer.BankInformationPublisherService;
6 | import com.ruubel.service.strategy.BankScraperStrategy;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.stereotype.Service;
9 |
10 | import java.util.ArrayList;
11 | import java.util.List;
12 |
13 | /**
14 | * Created by indrek.ruubel on 02/07/2016.
15 | */
16 | @Service
17 | public class BankService {
18 |
19 | private BankInformationPublisherService bankInformationPublisherService;
20 | private ScraperFactoryService scraperFactoryService;
21 |
22 | @Autowired
23 | public BankService(BankInformationPublisherService bankInformationPublisherService, ScraperFactoryService scraperFactoryService) {
24 | this.bankInformationPublisherService = bankInformationPublisherService;
25 | this.scraperFactoryService = scraperFactoryService;
26 | }
27 |
28 | public List getContacts() {
29 | List bankInformations = new ArrayList<>();
30 | for (BankScraperStrategy strategy : scraperFactoryService.getStrategies()) {
31 | BankInformation bank = strategy.scrape();
32 | bankInformations.add(bank);
33 | }
34 | bankInformationPublisherService.publish(bankInformations);
35 | return bankInformations;
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/com/ruubel/service/factory/ScraperFactoryService.java:
--------------------------------------------------------------------------------
1 | package com.ruubel.service.factory;
2 |
3 | import com.ruubel.service.strategy.BankScraperStrategy;
4 | import com.ruubel.service.strategy.HttpFetchService;
5 | import com.ruubel.service.strategy.SebScraper;
6 | import com.ruubel.service.strategy.SwedbankScraper;
7 | import org.springframework.stereotype.Service;
8 |
9 | import java.util.ArrayList;
10 | import java.util.List;
11 |
12 | /**
13 | * Created by indrek.ruubel on 03/07/2016.
14 | *
15 | * Factory pattern:
16 | * - creates objects without exposing the instantiation logic to the client.
17 | * - refers to the newly created object through a common interface
18 | * https://www.oodesign.com/factory-pattern.html
19 | */
20 | @Service
21 | public class ScraperFactoryService {
22 |
23 | private List strategies;
24 | private HttpFetchService httpFetchService;
25 |
26 | public ScraperFactoryService() {
27 | httpFetchService = new HttpFetchService();
28 | strategies = createStrategies();
29 | }
30 |
31 | /**
32 | * Internally creates objects, does not expose instantiation logic to the client.
33 | */
34 | private List createStrategies() {
35 | return new ArrayList() {{
36 | add(new SebScraper(httpFetchService));
37 | add(new SwedbankScraper(httpFetchService));
38 | }};
39 | }
40 |
41 | /**
42 | * Refers to the newly created object through a common interface
43 | */
44 | public List getStrategies() {
45 | return strategies;
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/com/ruubel/service/observer/BankInformationPublisherService.java:
--------------------------------------------------------------------------------
1 | package com.ruubel.service.observer;
2 |
3 | import com.ruubel.model.BankInformation;
4 | import org.springframework.stereotype.Service;
5 |
6 | import java.util.ArrayList;
7 | import java.util.List;
8 |
9 | /**
10 | * Created by indrek.ruubel on 03/07/2016.
11 | *
12 | * Observer pattern:
13 | * Defines a one-to-many dependency between objects so that when one object changes state,
14 | * all its dependents are notified and updated automatically.
15 | * https://www.oodesign.com/observer-pattern.html
16 | */
17 | @Service
18 | public class BankInformationPublisherService {
19 |
20 | private List subscribers;
21 |
22 | public BankInformationPublisherService() {
23 | subscribers = new ArrayList<>();
24 | }
25 |
26 | /**
27 | * Services can "sign up" here to receive updates
28 | * @param subscriber
29 | */
30 | public void subscribe(BankInformationReceived subscriber) {
31 | subscribers.add(subscriber);
32 | }
33 |
34 | /**
35 | * Service can "opt-out" from receiving these updates
36 | * @param subscriber
37 | */
38 | public void unsubscribe(BankInformationReceived subscriber) {
39 | subscribers.remove(subscriber);
40 | }
41 |
42 | /**
43 | * This is called when desired event happens, all subscribers will be informed
44 | */
45 | public void publish(List data) {
46 | for (BankInformationReceived subscriber : subscribers) {
47 | subscriber.receivedBankInformation(data);
48 | }
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/java/com/ruubel/service/observer/BankInformationReceived.java:
--------------------------------------------------------------------------------
1 | package com.ruubel.service.observer;
2 |
3 | import com.ruubel.model.BankInformation;
4 |
5 | import java.util.List;
6 |
7 | /**
8 | * Created by indrek.ruubel on 03/07/2016.
9 | *
10 | * All services that subscribe to BankInformationPublisherService for updates need to implement
11 | * this interface to receive updates.
12 | */
13 | public interface BankInformationReceived {
14 | void receivedBankInformation(List data);
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/com/ruubel/service/observer/PrinterService.java:
--------------------------------------------------------------------------------
1 | package com.ruubel.service.observer;
2 |
3 | import com.ruubel.model.BankInformation;
4 | import org.springframework.beans.factory.annotation.Autowired;
5 | import org.springframework.stereotype.Service;
6 |
7 | import java.util.List;
8 |
9 | /**
10 | * Created by indrek.ruubel on 03/07/2016.
11 | *
12 | * This service is very interested in events that take place in BankService,
13 | * so it subscribes itself to BankInformationPublisherService.
14 | * This service chooses to print out the results (for demo sake).
15 | */
16 | @Service
17 | public class PrinterService implements BankInformationReceived {
18 |
19 | private BankInformationPublisherService bankInformationPublisherService;
20 |
21 | @Autowired
22 | public PrinterService(BankInformationPublisherService bankInformationPublisherService) {
23 | this.bankInformationPublisherService = bankInformationPublisherService;
24 | this.bankInformationPublisherService.subscribe(this);
25 | }
26 |
27 | @Override
28 | public void receivedBankInformation(List data) {
29 | System.out.println("Printing: " + data);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/com/ruubel/service/observer/SaverService.java:
--------------------------------------------------------------------------------
1 | package com.ruubel.service.observer;
2 |
3 | import com.ruubel.model.BankInformation;
4 | import org.springframework.beans.factory.annotation.Autowired;
5 | import org.springframework.stereotype.Service;
6 |
7 | import java.util.List;
8 |
9 | /**
10 | * Created by indrek.ruubel on 03/07/2016.
11 | *
12 | * This service is very interested in events that take place in BankService,
13 | * so it subscribes itself to BankInformationPublisherService.
14 | * This service chooses to "save" the results (for demo sake).
15 | */
16 | @Service
17 | public class SaverService implements BankInformationReceived {
18 |
19 | private BankInformationPublisherService bankInformationPublisherService;
20 |
21 | @Autowired
22 | public SaverService(BankInformationPublisherService bankInformationPublisherService) {
23 | this.bankInformationPublisherService = bankInformationPublisherService;
24 | this.bankInformationPublisherService.subscribe(this);
25 | }
26 |
27 | @Override
28 | public void receivedBankInformation(List data) {
29 | System.out.println("Saving: " + data);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/com/ruubel/service/strategy/BankScraperStrategy.java:
--------------------------------------------------------------------------------
1 | package com.ruubel.service.strategy;
2 |
3 | import com.ruubel.model.BankInformation;
4 |
5 | /**
6 | * Created by indrek.ruubel on 02/07/2016.
7 | *
8 | * Strategy pattern:
9 | * Define a family of algorithms, encapsulate each one, and make them interchangeable.
10 | * Strategy lets the algorithm vary independently from clients that use it.
11 | * https://www.oodesign.com/strategy-pattern.html
12 | */
13 | public interface BankScraperStrategy {
14 | BankInformation scrape();
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/com/ruubel/service/strategy/HttpFetchService.java:
--------------------------------------------------------------------------------
1 | package com.ruubel.service.strategy;
2 |
3 | import org.jsoup.Jsoup;
4 | import org.jsoup.nodes.Document;
5 |
6 | import java.io.IOException;
7 |
8 | /**
9 | * A wrapper service for testing purposes
10 | */
11 | public class HttpFetchService {
12 |
13 | public Document get(String url) throws IOException {
14 | return Jsoup.connect(url).get();
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/com/ruubel/service/strategy/SebScraper.java:
--------------------------------------------------------------------------------
1 | package com.ruubel.service.strategy;
2 |
3 | import com.ruubel.model.Bank;
4 | import com.ruubel.model.BankInformation;
5 | import org.jsoup.nodes.Document;
6 | import org.jsoup.select.Elements;
7 |
8 | /**
9 | * Created by indrek.ruubel on 02/07/2016.
10 | */
11 | public class SebScraper implements BankScraperStrategy {
12 |
13 | private String bankUrl = "http://www.seb.ee/eng/contact/contact";
14 | private HttpFetchService httpFetchService;
15 |
16 | public SebScraper(HttpFetchService httpFetchService) {
17 | this.httpFetchService = httpFetchService;
18 | }
19 |
20 | @Override
21 | public BankInformation scrape() {
22 | String number = "FAILED";
23 | try {
24 | Document doc = httpFetchService.get(bankUrl);
25 |
26 | Elements content = doc.select(".field-type-text-with-summary");
27 | Elements tables = content.get(0).select("table");
28 | Elements tds = tables.get(0).select("td");
29 | number = tds.get(3).text();
30 | } catch (Exception e) {
31 | e.printStackTrace();
32 | }
33 |
34 | return new BankInformation(Bank.SEB, number);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/com/ruubel/service/strategy/SwedbankScraper.java:
--------------------------------------------------------------------------------
1 | package com.ruubel.service.strategy;
2 |
3 | import com.ruubel.model.Bank;
4 | import com.ruubel.model.BankInformation;
5 | import org.jsoup.nodes.Document;
6 | import org.jsoup.nodes.Element;
7 | import org.jsoup.select.Elements;
8 |
9 | /**
10 | * Created by indrek.ruubel on 02/07/2016.
11 | */
12 | public class SwedbankScraper implements BankScraperStrategy {
13 |
14 | private String bankUrl = "https://www.swedbank.ee/private/home/more/channels?language=EST";
15 | private HttpFetchService httpFetchService;
16 |
17 | public SwedbankScraper(HttpFetchService httpFetchService) {
18 | this.httpFetchService = httpFetchService;
19 | }
20 |
21 | @Override
22 | public BankInformation scrape() {
23 | String number = "FAILED";
24 | try {
25 | Document doc = httpFetchService.get(bankUrl);
26 |
27 | Elements footers = doc.select("section.footer-section");
28 | Element tel = footers.get(0).select("div.tel").get(0);
29 | number = tel.text();
30 | } catch (Exception e) {
31 | e.printStackTrace();
32 | }
33 |
34 | return new BankInformation(Bank.SWEDBANK, number);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/test/groovy/com/ruubel/aspect/AroundGetContactsAspectSpec.groovy:
--------------------------------------------------------------------------------
1 | package com.ruubel.aspect
2 |
3 | import org.aspectj.lang.ProceedingJoinPoint
4 | import spock.lang.Specification
5 |
6 | class AroundGetContactsAspectSpec extends Specification {
7 |
8 | AroundGetContactsAspect aspect
9 |
10 | def setup () {
11 | aspect = new AroundGetContactsAspect()
12 | }
13 |
14 | def "when ProceedingJoinPoint is passed, then returns the result of the proceed" () {
15 | given:
16 | ProceedingJoinPoint pjp = Mock(ProceedingJoinPoint)
17 | String out = "test"
18 | when:
19 | Object result = aspect.profile(pjp)
20 | then:
21 | 1 * pjp.proceed() >> out
22 | result == out
23 |
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/src/test/groovy/com/ruubel/controller/BankControllerSpec.groovy:
--------------------------------------------------------------------------------
1 | package com.ruubel.controller
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper
4 | import com.fasterxml.jackson.databind.ObjectWriter
5 | import com.ruubel.model.Bank
6 | import com.ruubel.model.BankInformation
7 | import com.ruubel.service.BankService
8 | import org.springframework.http.HttpStatus
9 | import org.springframework.http.MediaType
10 | import org.springframework.test.web.servlet.MockMvc
11 | import org.springframework.test.web.servlet.setup.MockMvcBuilders
12 | import spock.lang.Specification
13 |
14 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
15 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
16 |
17 | class BankControllerSpec extends Specification {
18 |
19 | BankController controller
20 | BankService bankService
21 |
22 | ObjectWriter ow
23 | MockMvc mockMvc
24 |
25 | def setup() {
26 | ObjectMapper mapper = new ObjectMapper()
27 | ow = mapper.writer().withDefaultPrettyPrinter()
28 |
29 | bankService = Mock(BankService)
30 | controller = new BankController(bankService)
31 |
32 | mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
33 | }
34 |
35 | def "when calling banks API endpoint [GET], then returns json 200 OK" () {
36 | when:
37 | def response = mockMvc.perform(get("/api/v1/banks")
38 | .contentType(MediaType.APPLICATION_JSON)).andReturn().response
39 | then:
40 | 1 * bankService.getContacts() >> [new BankInformation(Bank.SEB, "12345")]
41 | response.status == HttpStatus.OK.value()
42 | response.contentAsString == "[{\"bank\":\"SEB\",\"phoneNumber\":\"12345\"}]"
43 | }
44 |
45 | def "when calling banks API endpoint [POST], then returns 405 METHOD_NOT_ALLOWED" () {
46 | when:
47 | def response = mockMvc.perform(post("/api/v1/banks")
48 | .contentType(MediaType.APPLICATION_JSON)).andReturn().response
49 | then:
50 | 0 * bankService.getContacts()
51 | response.status == HttpStatus.METHOD_NOT_ALLOWED.value()
52 | response.contentAsString == ""
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/src/test/groovy/com/ruubel/service/BankServiceSpec.groovy:
--------------------------------------------------------------------------------
1 | package com.ruubel.service
2 |
3 | import com.ruubel.model.Bank
4 | import com.ruubel.model.BankInformation
5 | import com.ruubel.service.factory.ScraperFactoryService
6 | import com.ruubel.service.observer.BankInformationPublisherService
7 | import com.ruubel.service.strategy.BankScraperStrategy
8 | import spock.lang.Specification
9 |
10 | class BankServiceSpec extends Specification {
11 |
12 | BankService service
13 | BankInformationPublisherService bankInformationPublisherService
14 | ScraperFactoryService scraperFactoryService
15 |
16 | def setup() {
17 | bankInformationPublisherService = Mock(BankInformationPublisherService)
18 | scraperFactoryService = Mock(ScraperFactoryService)
19 | service = new BankService(bankInformationPublisherService, scraperFactoryService)
20 | }
21 |
22 | def "when empty list of strategies is defined, then scrapes none, publishes and returns empty list" () {
23 | when:
24 | List contacts = service.getContacts()
25 | then:
26 | 1 * service.scraperFactoryService.getStrategies() >> []
27 | 1 * bankInformationPublisherService.publish([])
28 | contacts == []
29 | }
30 |
31 | def "when strategy is defined, then scrapes it, publishes and returns on element list" () {
32 | given:
33 | BankScraperStrategy scraper = Mock(BankScraperStrategy)
34 | BankInformation scrapeResult = new BankInformation(Bank.SEB, "12345")
35 | when:
36 | List contacts = service.getContacts()
37 | then:
38 | 1 * scraperFactoryService.getStrategies() >> [scraper]
39 | 1 * scraper.scrape() >> scrapeResult
40 | 1 * bankInformationPublisherService.publish([scrapeResult])
41 | contacts == [scrapeResult]
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/src/test/groovy/com/ruubel/service/factory/ScraperFactoryServiceSpec.groovy:
--------------------------------------------------------------------------------
1 | package com.ruubel.service.factory
2 |
3 | import com.ruubel.service.strategy.BankScraperStrategy
4 | import spock.lang.Specification
5 |
6 | class ScraperFactoryServiceSpec extends Specification {
7 |
8 | ScraperFactoryService service
9 |
10 | def setup() {
11 | service = new ScraperFactoryService()
12 | }
13 |
14 | def "when service is initialized, then has strategies setup" () {
15 | expect:
16 | service.strategies.size() == 2
17 | }
18 |
19 | def "when getting strategies, then returns the exact list" () {
20 | when:
21 | List strategies = service.getStrategies()
22 | then:
23 | service.strategies == strategies
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/src/test/groovy/com/ruubel/service/observer/BankInformationPublisherServiceSpec.groovy:
--------------------------------------------------------------------------------
1 | package com.ruubel.service.observer
2 |
3 | import com.ruubel.model.Bank
4 | import com.ruubel.model.BankInformation
5 | import spock.lang.Specification
6 |
7 | class BankInformationPublisherServiceSpec extends Specification {
8 |
9 | BankInformationPublisherService service
10 |
11 | def setup() {
12 | service = new BankInformationPublisherService()
13 | }
14 |
15 | def "when subscribes, then gets added to internal subscriber list" () {
16 | given:
17 | BankInformationReceived subscriber = Mock(BankInformationReceived)
18 | when:
19 | service.subscribe(subscriber)
20 | then:
21 | service.subscribers == [subscriber]
22 | }
23 |
24 | def "when data is published, then subscriber receives data" () {
25 | given:
26 | List data = [new BankInformation(Bank.SEB, "12345")]
27 | BankInformationReceived subscriber = Mock(BankInformationReceived)
28 | service.subscribe(subscriber)
29 | when:
30 | service.publish(data)
31 | then:
32 | 1 * subscriber.receivedBankInformation(data)
33 | }
34 |
35 | def "when subscriber unsubscribes, internal list becomes empty" () {
36 | given:
37 | BankInformationReceived subscriber = Mock(BankInformationReceived)
38 | service.subscribe(subscriber)
39 | when:
40 | service.unsubscribe(subscriber)
41 | then:
42 | service.subscribers == []
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/src/test/groovy/com/ruubel/service/strategy/SebScraperSpec.groovy:
--------------------------------------------------------------------------------
1 | package com.ruubel.service.strategy
2 |
3 | import com.ruubel.model.Bank
4 | import com.ruubel.model.BankInformation
5 | import org.jsoup.Jsoup
6 | import spock.lang.Specification
7 |
8 | class SebScraperSpec extends Specification {
9 |
10 | SebScraper scraper
11 | HttpFetchService httpFetchService
12 |
13 | def setup () {
14 | httpFetchService = Mock(HttpFetchService)
15 | scraper = new SebScraper(httpFetchService)
16 | }
17 |
18 | def "when fetches not expected HTML, then phoneNumber is FAILED" () {
19 | given:
20 | httpFetchService.get(_) >> Jsoup.parse("")
22 | when:
23 | BankInformation bankInformation = scraper.scrape()
24 | then:
25 | bankInformation.getBank() == Bank.SEB
26 | bankInformation.getPhoneNumber() == "FAILED"
27 | }
28 |
29 | def "when fetches expected HTML, then retrieves the phoneNumber as expected" () {
30 | given:
31 | String phoneNumber = "12345"
32 | httpFetchService.get(_) >> Jsoup.parse("")
34 | when:
35 | BankInformation bankInformation = scraper.scrape()
36 | then:
37 | bankInformation.getBank() == Bank.SEB
38 | bankInformation.getPhoneNumber() == phoneNumber
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/src/test/groovy/com/ruubel/service/strategy/SwebankScraperSpec.groovy:
--------------------------------------------------------------------------------
1 | package com.ruubel.service.strategy
2 |
3 | import com.ruubel.model.Bank
4 | import com.ruubel.model.BankInformation
5 | import org.jsoup.Jsoup
6 | import spock.lang.Specification
7 |
8 | class SwebankScraperSpec extends Specification {
9 |
10 | SwedbankScraper scraper
11 | HttpFetchService httpFetchService
12 |
13 | def setup () {
14 | httpFetchService = Mock(HttpFetchService)
15 | scraper = new SwedbankScraper(httpFetchService)
16 | }
17 |
18 | def "when fetches not expected HTML, then phoneNumber is FAILED" () {
19 | given:
20 | httpFetchService.get(_) >> Jsoup.parse("")
22 | when:
23 | BankInformation bankInformation = scraper.scrape()
24 | then:
25 | bankInformation.getBank() == Bank.SWEDBANK
26 | bankInformation.getPhoneNumber() == "FAILED"
27 | }
28 |
29 | def "when fetches expected HTML, then retrieves the phoneNumber as expected" () {
30 | given:
31 | String phoneNumber = "12345"
32 | httpFetchService.get(_) >> Jsoup.parse("")
34 | when:
35 | BankInformation bankInformation = scraper.scrape()
36 | then:
37 | bankInformation.getBank() == Bank.SWEDBANK
38 | bankInformation.getPhoneNumber() == phoneNumber
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------