├── .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 | [![CircleCI](https://circleci.com/gh/indrekru/design-patterns-spring-boot.svg?style=svg)](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("
" + 21 | "
") 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("
" + 33 | "
" + phoneNumber + "
") 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("
" + 21 | "
") 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("
" + 33 | "
" + phoneNumber + "
") 34 | when: 35 | BankInformation bankInformation = scraper.scrape() 36 | then: 37 | bankInformation.getBank() == Bank.SWEDBANK 38 | bankInformation.getPhoneNumber() == phoneNumber 39 | } 40 | 41 | } 42 | --------------------------------------------------------------------------------