ECE366 - Lesson 10

Testing, Stomp

Instructor: Professor Hong

Review & Questions

## Testing
## Make a basic spring boot project - [https://start.spring.io/](https://start.spring.io/)
## Add Sample Class ``` package com.example.demo; public class Calculator { public int multiplyByTwo(int x) { return 2*x; } } ```
## Add Sample Tests ``` package com.example.demo; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest class DemoApplicationTests { @Test void contextLoads() { } @Test void testMultiplyByTwo() { Calculator c = new Calculator(); int result = c.multiplyByTwo(2); assertEquals(4, result); } } ```
## Testing Resources [https://medium.com/capital-one-tech/improve-java-code-with-unit-tests-and-jacoco-b342643736ed](https://medium.com/capital-one-tech/improve-java-code-with-unit-tests-and-jacoco-b342643736ed) [https://www.jetbrains.com/help/idea/running-test-with-coverage.html#run-config-with-coverage](https://www.jetbrains.com/help/idea/running-test-with-coverage.html#run-config-with-coverage)
## Java Testing
## Types of Testing - Unit testing - smallest pieces possible - System testing - bigger part, complete API - Integration testing - connecting different systems - Acceptance testing - end user testing - Performance testing - speed of program - Regression testing - testing everything again - Security testing - spot weaknesses in security - Load testing - large loads / lots of hits - End-to-end testing - entire system
## TDD - Test Driven Development - Written before implementation - Tests will fail first and then succeed after writing code - Forces developer to think about requirements - Not skipped due to time pressure - Bugs caught early - Still need system and integration tests
## Advantages of Unit Testing - Validating smallest units of software - Find bugs easily and early - Save time and money in the long run - Forcing developers to write better and cleaner code
## JUnit - Unit testing framework for Java - Part of xUnit series - Enables automated unit testing
## JUnit and Maven - Create a new Maven project - Add Maven dependency ``` <dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.2</version> <scope>test</scope> </dependency> </dependencies> ```
## Your First Test src/main/java/Code.java ``` public class Code { public String sayHello() { return "Hello world!"; } } ```
## Your First Test test/java/CodeTest.java ``` import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; public class CodeTest { @Test public void testSayHello() { Code code = new Code(); assertEquals("Hello world!", code.sayHello()); } } ```
## Basic Tests
## Bank Account src/java/BankAccount.java ``` public class BankAccount { private double balance; private double minimumBalance; public BankAccount(double balance, double minimumBalance) { this.balance = balance; this.minimumBalance = minimumBalance; } public double getBalance() { return balance; } public double getMinimumBalance() { return minimumBalance; } public double withdraw(double amount) { if(balance - amount > minimumBalance) { balance -= amount; return amount; } else { throw new RuntimeException(); } } public double deposit(double amount) { try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } return balance += amount; } } ```
## Bank Account Tests test/java/BankAccountTest.java ``` import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.time.Duration; import static org.junit.jupiter.api.Assertions.*; @DisplayName("Test BankAccount class") public class BankAccountTest { @Test @DisplayName("Withdraw 500 successfully") public void testWithdraw() { BankAccount bankAccount = new BankAccount(500, -1000); bankAccount.withdraw(300); assertEquals(200, bankAccount.getBalance()); } @Test @DisplayName("Deposit 500 successfully") public void testDeposit() { BankAccount bankAccount = new BankAccount(400, 0); bankAccount.deposit(500); assertEquals(900, bankAccount.getBalance(), "Unexpected value, expected 900"); } } ``` - DisplayName gives the tests a better name/description
## Assertions - Check the outcome of the test - If the assertion fails, the test fails - Assertions class in the org.junit.jupiter.api package - Many methods and overrides - Add isActive and holderName
## Additional Properties for BankAccount src/java/BankAccount.java ``` private boolean isActive = true; private String holderName; public boolean isActive() { return isActive; } public void setActive(boolean active) { isActive = active; } public String getHolderName() { return holderName; } public void setHolderName(String holderName) { this.holderName = holderName; } ```
## Additional BankAccount Tests tests/java/BankAccountAssertionTest.java ``` import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.time.Duration; import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertTimeout; public class BankAccountAssertionsTest { @Test @DisplayName("Withdraw will become negative") public void testWithdrawNotStuckAtZero() { BankAccount bankAccount = new BankAccount(500, -1000); bankAccount.withdraw(800); assertNotEquals(0, bankAccount.getBalance()); } @Test @DisplayName("Test activation account after creation") public void testActive() { BankAccount bankAccount = new BankAccount(500, 0); assertTrue(bankAccount.isActive()); } @Test @DisplayName("Test set holder name") public void testHolderNameSet() { BankAccount bankAccount = new BankAccount(500, 0); bankAccount.setHolderName("Chris"); assertNotNull(bankAccount.getHolderName()); } @Test @DisplayName("Test that we can't withdraw below the minimum") public void testNoWithdrawBelowMinimum() { BankAccount bankAccount = new BankAccount(500, -1000); assertThrows(RuntimeException.class, () -> bankAccount.withdraw(2000)); } @Test @DisplayName("Test no exceptions for withdraw and deposit") public void testWithdrawAndDepositWithoutException() { BankAccount bankAccount = new BankAccount(500, -1000); assertAll(() -> bankAccount.deposit(200), () -> bankAccount.withdraw(450)); } @Test @DisplayName("Test speed deposit") public void testDepositTimeout() { BankAccount bankAccount = new BankAccount(400, 0); assertTimeout(Duration.ofSeconds(1), () -> bankAccount.deposit(200)); } // assertEquals, delta, message // fail(); // fails the test } ```
## Assumptions - Setting a condition for executing a test - If the assumption is met, the test will be executed - If the assumption is not met, the test won't be executed - Assumptions are in the org.junit.jupiter.api package
## Assumptions Test tests/java/BankAccountAssumptionsTest.java ``` import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.*; public class BankAccountAssumptionsTest { @Test @DisplayName("Test activation account after creation") public void testActive() { BankAccount bankAccount = new BankAccount(500, 0); assumeTrue(bankAccount != null); assumeFalse(bankAccount == null); assumingThat(bankAccount == null, () -> assertTrue(bankAccount.isActive())); // doesn't abort the test and continues assertTrue(bankAccount.isActive()); } } ```
## Test Order - Without specifying, we cannot predict the order of the tests - Usually, this isn't a problem as the tests should be independent - If you do need to order your tests, you can leverage the Order annotation
## Ordered Execution Tests tests/java/BankAccountOrderedExecutionTest.java ``` import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import static org.junit.jupiter.api.Assertions.assertEquals; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class BankAccountOrderedExecutionTest { static BankAccount bankAccount = new BankAccount(0,0); @Test @Order(2) public void testWithdraw() { bankAccount.withdraw(300); assertEquals(200, bankAccount.getBalance()); } @Test @Order(1) public void testDeposit() { bankAccount.deposit(500); assertEquals(500, bankAccount.getBalance()); } } ```
## Nested Tests - Used to control the relationship between tests - Useful when you have feature separation or when code is organized around a method or feature
## Nested Tests tests/java/BankAccountNestedTest.java ``` import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; public class BankAccountNestedTest { @Test @DisplayName("Withdraw 500 successfully") public void testWithdraw() { BankAccount bankAccount = new BankAccount(500, -1000); bankAccount.withdraw(300); assertEquals(200, bankAccount.getBalance()); } @Test @DisplayName("Deposit 400 successfully") public void testDeposit() { BankAccount bankAccount = new BankAccount(400, 0); bankAccount.deposit(500); assertEquals(900, bankAccount.getBalance()); } @Nested class WhenBalanceEqualsZero { @Test @DisplayName("Withdrawing below minimum balance: exception") public void testWithdrawMinimumBalanceIs0() { BankAccount bankAccount = new BankAccount(0, 0); assertThrows(RuntimeException.class, () -> bankAccount.withdraw(500)); } @Test @DisplayName("Wtihdrawing below minimum balance: negative balance") public void testWithdrawMinimumBalanceNegative1000() { BankAccount bankAccount = new BankAccount(0, -1000); bankAccount.withdraw(500); assertEquals(-500, bankAccount.getBalance()); } } } ```
## More Advanced Tests
## Dependency Injection - Less tightly coupled classes - No need to manually create instances everytime - No need to create new BankAccount(0,0) any more
## Parameter Resolver tests/java/BankAccountParameterResolver.java ``` import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; public class BankAccountParameterResolver implements ParameterResolver { @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { return parameterContext.getParameter().getType() == BankAccount.class; } @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { return new BankAccount(0, 0); } } ```
## Dependency Injection Test tests/java/BankAccountDITest.java ``` import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import static org.junit.jupiter.api.Assertions.assertEquals; @ExtendWith(BankAccountParameterResolver.class) public class BankAccountDITest { @Test @DisplayName("Deposit 500 successfully") public void testDeposit(BankAccount bankAccount) { bankAccount.deposit(500); assertEquals(500, bankAccount.getBalance()); } } ```
## Repeated Tests tests/java/BankAccountRepeatedTest.java ``` import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.RepetitionInfo; import org.junit.jupiter.api.extension.ExtendWith; import static org.junit.jupiter.api.Assertions.assertEquals; @ExtendWith(BankAccountParameterResolver.class) public class BankAccountRepeatedTest { @RepeatedTest(5) @DisplayName("Deposit 400 successfully") public void testDeposit(BankAccount bankAccount) { bankAccount.deposit(500); assertEquals(500, bankAccount.getBalance()); } @RepeatedTest(5) @DisplayName("Deposit 400 successfully") public void testDepositRepetitionInfo(BankAccount bankAccount, RepetitionInfo repetitionInfo) { bankAccount.deposit(500); assertEquals(500, bankAccount.getBalance()); System.out.println("Nr: " + repetitionInfo.getCurrentRepetition()); } } ``` Great for repeating tests multiple times.
## Parameterized Tests tests/java/BankAccountParameterizedTest.java ``` import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvFileSource; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; import java.time.DayOfWeek; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @ExtendWith(BankAccountParameterResolver.class) public class BankAccountParameterizedTest { @ParameterizedTest @ValueSource(ints = {100, 400, 800, 1000}) @DisplayName("Deposit successfully") public void testDeposit(int amount, BankAccount bankAccount) { bankAccount.deposit(amount); assertEquals(amount, bankAccount.getBalance()); } @ParameterizedTest @EnumSource(value = DayOfWeek.class, names = {"TUESDAY", "THURSDAY"}) public void testDayOfWeek(DayOfWeek day) { assertTrue(day.toString().startsWith("T")); } @ParameterizedTest @CsvSource({"100, Mary", "200, Richard", "150, Ted"}) //@CsvFileSource(resources = "details.csv", delimiter = ',') public void depositAndNameTest(double amount, String name, BankAccount bankAccount) { bankAccount.deposit(amount); bankAccount.setHolderName(name); assertEquals(amount, bankAccount.getBalance()); assertEquals(name, bankAccount.getHolderName()); } } ``` - Run test several times with different parameters
## Timeout Tests tests/java/BankAccountTimeoutTest.java ``` import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.extension.ExtendWith; import java.time.Duration; import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTimeout; // @Timeout(value=500, unit=imeUnit.MILLISECONDS) @ExtendWith(BankAccountParameterResolver.class) public class BankAccountTimeoutTest { @Test @Timeout(value=500, unit= TimeUnit.MILLISECONDS) public void testDepositTimeoutAssertion(BankAccount bankAccount) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } bankAccount.deposit(300); assertEquals(300, bankAccount.getBalance()); } @Test public void testDepositTimeoutAnnotation(BankAccount bankAccount) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } bankAccount.deposit(300); assertTimeout(Duration.ofMillis(500), () -> { Thread.sleep(10); }); } } ```
## Parallel Execution Setup tests/resources/junit-platform.properties ``` junit.jupiter.execution.parallel.enabled=true junit.jupiter.execution.parallel.config.strategy=dynamic ```
## Parallel Execution tests/java/BankAccountParallelExecutionTest.java ``` import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; import static org.junit.jupiter.api.Assertions.assertEquals; @Execution(ExecutionMode.CONCURRENT) @ExtendWith(BankAccountParameterResolver.class) public class BankAccountParallelExecutionTest { @Test @DisplayName("Deposit 500 successfully") public void testDeposit(BankAccount bankAccount) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } bankAccount.deposit(500); assertEquals(500, bankAccount.getBalance()); } @Test @DisplayName("Deposit 500 successfully") public void testDeposit2(BankAccount bankAccount) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } bankAccount.deposit(500); assertEquals(500, bankAccount.getBalance()); } @Test @DisplayName("Deposit 500 successfully") public void testDeposit3(BankAccount bankAccount) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } bankAccount.deposit(500); assertEquals(500, bankAccount.getBalance()); } } ```
## Before and After Test tests/java/BankAccountBeforeAndAfterTest.java ``` import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.assertEquals; @TestInstance(TestInstance.Lifecycle.PER_CLASS) // can use this for BeforeAll/AfterAll w/o static public class BankAccountBeforeAndAfterTest { static BankAccount bankAccount; @BeforeAll //@BeforeEach // fn must not be static public void prepTest() { System.out.println("Hi"); bankAccount = new BankAccount(500, 0); } @Test public void testWithdraw() { bankAccount.withdraw(300); assertEquals(200, bankAccount.getBalance()); } @Test public void testDeposit() { bankAccount.deposit(500); assertEquals(1000, bankAccount.getBalance()); } @AfterAll //@AfterEach // fn must not be static public void endTest() { System.out.println("Bye"); } } ```
## Conditional Tests tests/java/BankAccountConditionalTest.java ``` import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.*; public class BankAccountConditionalTest { @Test @EnabledOnOs({OS.MAC}) public void testMac() { } @Test @EnabledOnOs({OS.WINDOWS}) public void testWindows() { } @Test @EnabledOnJre({JRE.JAVA_16}) public void testJRE() { } @Test @DisabledOnJre({JRE.JAVA_16}) public void testNoJRE16() { } } ``` Generally not good practice
## Disable a Test tests/java/BankAccountDisabledTest.java ``` import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import static org.junit.jupiter.api.Assertions.assertEquals; @ExtendWith(BankAccountParameterResolver.class) public class BankAccountDisabledTest { @Test @Disabled("Temporarily disabled due to maintenance") @DisplayName("Deposit 500 successfully") public void testDeposit() { BankAccount bankAccount = new BankAccount(400, 0); bankAccount.deposit(500); assertEquals(900, bankAccount.getBalance(), "Unexpected value, expected 900"); } } ```
## Mockito - A way to mock functions/systems pom.xml ``` <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <version>2.23.4</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>3.8.0</version> <scope>test</scope> </dependency> ```
## Calculate Methods src/main/java/CalculateMethods.java ``` public class CalculateMethods { public double divide(int x, int y) { return x / y; } } ```
## Mockito Test test/java/CalculateMethodsMockitoTest.java ``` import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.assertEquals; @ExtendWith(MockitoExtension.class) public class CalculateMethodsMockitoTest { @Mock CalculateMethods calculateMethods; @BeforeEach public void setupMocks() { Mockito.when(calculateMethods.divide(6,3)).thenReturn(2.0); } @Test public void testDivide() { assertEquals(2.0, calculateMethods.divide(6,3)); } } ```
## Running Reports
## Surefire Reports mvn.pom ``` <build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> </plugin> <plugin> <artifactId>maven-failsafe-plugin</artifactId> <version>2.22.2</version> </plugin> </plugins> </build> ``` ``` mvn surefire-report:report ```
## Jacoco pom.xml ``` <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.9</version> <executions> <execution> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <phase>prepare-package</phase> <goals> <goal>report</goal> </goals> </execution> </executions> </plugin> ``` ``` mvn jacoco:report ```
## Best Practices - Keep it simple - Test the actual unit of code - Clear naming and stick to naming conventions - Low cyclomatic complexity - the fewer codes in path, the better - Shouldn't have implementation in code - Deterministic tests - you should always have the same result
## Websocket + STOMP
## STOMP - Simple/Streaming Text Oriented Message Protocol - Allows you to create an interactive web application - The web is the STOMP client - The service is the message broker - Example from [https://spring.io/guides/gs/messaging-stomp-websocket/](https://spring.io/guides/gs/messaging-stomp-websocket/)
## Pre-initialized Spring Initializr [Pre-initialized Initializr](https://start.spring.io/#!type=maven-project&groupId=com.example&artifactId=messaging-stomp-websocket&name=messaging-stomp-websocket&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.messaging-stomp-websocket&dependencies=websocket) Additional pom.xml dependencies ``` <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator-core</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>sockjs-client</artifactId> <version>1.0.2</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>stomp-websocket</artifactId> <version>2.3.3</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>bootstrap</artifactId> <version>5.3.2</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.6.4</version> </dependency> ```
## HelloMessage Class HelloMessage.java ``` package com.example.messagingstompwebsocket; public class HelloMessage { private String name; public HelloMessage() { } public HelloMessage(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } } ```
## Greeting Class Greeting.java ``` package com.example.messagingstompwebsocket; public class Greeting { private String content; public Greeting() { } public Greeting(String content) { this.content = content; } public String getContent() { return content; } } ```
## GreetingController Class GreetingController.java ``` package com.example.messagingstompwebsocket; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller; import org.springframework.web.util.HtmlUtils; @Controller public class GreetingController { @MessageMapping("/hello") @SendTo("/topic/greetings") public Greeting greeting(HelloMessage message) throws Exception { Thread.sleep(1000); // simulated delay return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"); } } ```
## WebSocketConfig Class WebSocketConfig.java ``` package com.example.messagingstompwebsocket; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/gs-guide-websocket").withSockJS(); } } ```
## Index.html src/main/resources/static/index.html ``` <!DOCTYPE html> <html> <head> <title>Hello WebSocket</title> <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="/main.css" rel="stylesheet"> <script src="/webjars/jquery/jquery.min.js"></script> <script src="/webjars/sockjs-client/sockjs.min.js"></script> <script src="/webjars/stomp-websocket/stomp.min.js"></script> <script src="/app.js"></script> </head> <body> <noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable Javascript and reload this page!</h2></noscript> <div id="main-content" class="container"> <div class="row"> <div class="col-md-6"> <form class="form-inline"> <div class="form-group"> <label for="connect">WebSocket connection:</label> <button id="connect" class="btn btn-default" type="submit">Connect</button> <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect </button> </div> </form> </div> <div class="col-md-6"> <form class="form-inline"> <div class="form-group"> <label for="name">What is your name?</label> <input type="text" id="name" class="form-control" placeholder="Your name here..."> </div> <button id="send" class="btn btn-default" type="submit">Send</button> </form> </div> </div> <div class="row"> <div class="col-md-12"> <table id="conversation" class="table table-striped"> <thead> <tr> <th>Greetings</th> </tr> </thead> <tbody id="greetings"> </tbody> </table> </div> </div> </div> </body> </html> ```
## app.js src/main/resources/static/app.js ``` var stompClient = null; function setConnected(connected) { $("#connect").prop("disabled", connected); $("#disconnect").prop("disabled", !connected); if (connected) { $("#conversation").show(); } else { $("#conversation").hide(); } $("#greetings").html(""); } function connect() { var socket = new SockJS('/gs-guide-websocket'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { setConnected(true); console.log('Connected: ' + frame); stompClient.subscribe('/topic/greetings', function (greeting) { showGreeting(JSON.parse(greeting.body).content); }); }); } function disconnect() { if (stompClient !== null) { stompClient.disconnect(); } setConnected(false); console.log("Disconnected"); } function sendName() { stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()})); } function showGreeting(message) { $("#greetings").append("" + message + ""); } $(function () { $("form").on('submit', function (e) { e.preventDefault(); }); $( "#connect" ).click(function() { connect(); }); $( "#disconnect" ).click(function() { disconnect(); }); $( "#send" ).click(function() { sendName(); }); }); ```