container.spec.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import { it, expect, describe, vi } from "vitest"
  2. import { Service } from "../lib/service"
  3. import { Container, currentContainer, ContainerEvent } from "../lib/container"
  4. class TestServiceA extends Service {
  5. public static ID = "TestServiceA"
  6. }
  7. class TestServiceB extends Service {
  8. public static ID = "TestServiceB"
  9. // Marked public to allow for testing
  10. public readonly serviceA = this.bind(TestServiceA)
  11. }
  12. describe("Container", () => {
  13. describe("getBoundServiceWithID", () => {
  14. it("returns the service instance if it is bound to the container", () => {
  15. const container = new Container()
  16. const service = container.bind(TestServiceA)
  17. expect(container.getBoundServiceWithID(TestServiceA.ID)).toBe(service)
  18. })
  19. it("returns undefined if the service is not bound to the container", () => {
  20. const container = new Container()
  21. expect(container.getBoundServiceWithID(TestServiceA.ID)).toBeUndefined()
  22. })
  23. })
  24. describe("bind", () => {
  25. it("correctly binds the service to it", () => {
  26. const container = new Container()
  27. const service = container.bind(TestServiceA)
  28. // @ts-expect-error getContainer is defined as a protected property, but we are leveraging it here to check
  29. expect(service.getContainer()).toBe(container)
  30. })
  31. it("after bind, the current container is set back to its previous value", () => {
  32. const originalValue = currentContainer
  33. const container = new Container()
  34. container.bind(TestServiceA)
  35. expect(currentContainer).toBe(originalValue)
  36. })
  37. it("dependent services are registered in the same container", () => {
  38. const container = new Container()
  39. const serviceB = container.bind(TestServiceB)
  40. // @ts-expect-error getContainer is defined as a protected property, but we are leveraging it here to check
  41. expect(serviceB.serviceA.getContainer()).toBe(container)
  42. })
  43. it("binding an already initialized service returns the initialized instance (services are singletons)", () => {
  44. const container = new Container()
  45. const serviceA = container.bind(TestServiceA)
  46. const serviceA2 = container.bind(TestServiceA)
  47. expect(serviceA).toBe(serviceA2)
  48. })
  49. it("binding a service which is a dependency of another service returns the same instance created from the dependency resolution (services are singletons)", () => {
  50. const container = new Container()
  51. const serviceB = container.bind(TestServiceB)
  52. const serviceA = container.bind(TestServiceA)
  53. expect(serviceB.serviceA).toBe(serviceA)
  54. })
  55. it("binding an initialized service as a dependency returns the same instance", () => {
  56. const container = new Container()
  57. const serviceA = container.bind(TestServiceA)
  58. const serviceB = container.bind(TestServiceB)
  59. expect(serviceB.serviceA).toBe(serviceA)
  60. })
  61. it("container emits an init event when an uninitialized service is initialized via bind and event only called once", () => {
  62. const container = new Container()
  63. const serviceFunc = vi.fn<
  64. [ContainerEvent & { type: "SERVICE_INIT" }],
  65. void
  66. >()
  67. container.getEventStream().subscribe((ev) => {
  68. if (ev.type === "SERVICE_INIT") {
  69. serviceFunc(ev)
  70. }
  71. })
  72. const instance = container.bind(TestServiceA)
  73. expect(serviceFunc).toHaveBeenCalledOnce()
  74. expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
  75. type: "SERVICE_INIT",
  76. serviceID: TestServiceA.ID,
  77. })
  78. })
  79. it("the bind event emitted has an undefined bounderID when the service is bound directly to the container", () => {
  80. const container = new Container()
  81. const serviceFunc = vi.fn<
  82. [ContainerEvent & { type: "SERVICE_BIND" }],
  83. void
  84. >()
  85. container.getEventStream().subscribe((ev) => {
  86. if (ev.type === "SERVICE_BIND") {
  87. serviceFunc(ev)
  88. }
  89. })
  90. container.bind(TestServiceA)
  91. expect(serviceFunc).toHaveBeenCalledOnce()
  92. expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
  93. type: "SERVICE_BIND",
  94. boundeeID: TestServiceA.ID,
  95. bounderID: undefined,
  96. })
  97. })
  98. it("the bind event emitted has the correct bounderID when the service is bound to another service", () => {
  99. const container = new Container()
  100. const serviceFunc = vi.fn<
  101. [ContainerEvent & { type: "SERVICE_BIND" }],
  102. void
  103. >()
  104. container.getEventStream().subscribe((ev) => {
  105. // We only care about the bind event of TestServiceA
  106. if (ev.type === "SERVICE_BIND" && ev.boundeeID === TestServiceA.ID) {
  107. serviceFunc(ev)
  108. }
  109. })
  110. container.bind(TestServiceB)
  111. expect(serviceFunc).toHaveBeenCalledOnce()
  112. expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
  113. type: "SERVICE_BIND",
  114. boundeeID: TestServiceA.ID,
  115. bounderID: TestServiceB.ID,
  116. })
  117. })
  118. })
  119. describe("hasBound", () => {
  120. it("returns true if the given service is bound to the container", () => {
  121. const container = new Container()
  122. container.bind(TestServiceA)
  123. expect(container.hasBound(TestServiceA)).toEqual(true)
  124. })
  125. it("returns false if the given service is not bound to the container", () => {
  126. const container = new Container()
  127. expect(container.hasBound(TestServiceA)).toEqual(false)
  128. })
  129. it("returns true when the service is bound because it is a dependency of another service", () => {
  130. const container = new Container()
  131. container.bind(TestServiceB)
  132. expect(container.hasBound(TestServiceA)).toEqual(true)
  133. })
  134. })
  135. describe("getEventStream", () => {
  136. it("returns an observable which emits events correctly when services are initialized", () => {
  137. const container = new Container()
  138. const serviceFunc = vi.fn<
  139. [ContainerEvent & { type: "SERVICE_INIT" }],
  140. void
  141. >()
  142. container.getEventStream().subscribe((ev) => {
  143. if (ev.type === "SERVICE_INIT") {
  144. serviceFunc(ev)
  145. }
  146. })
  147. container.bind(TestServiceB)
  148. expect(serviceFunc).toHaveBeenCalledTimes(2)
  149. expect(serviceFunc).toHaveBeenNthCalledWith(1, <ContainerEvent>{
  150. type: "SERVICE_INIT",
  151. serviceID: TestServiceA.ID,
  152. })
  153. expect(serviceFunc).toHaveBeenNthCalledWith(2, <ContainerEvent>{
  154. type: "SERVICE_INIT",
  155. serviceID: TestServiceB.ID,
  156. })
  157. })
  158. it("returns an observable which emits events correctly when services are bound", () => {
  159. const container = new Container()
  160. const serviceFunc = vi.fn<
  161. [ContainerEvent & { type: "SERVICE_BIND" }],
  162. void
  163. >()
  164. container.getEventStream().subscribe((ev) => {
  165. if (ev.type === "SERVICE_BIND") {
  166. serviceFunc(ev)
  167. }
  168. })
  169. container.bind(TestServiceB)
  170. expect(serviceFunc).toHaveBeenCalledTimes(2)
  171. expect(serviceFunc).toHaveBeenNthCalledWith(1, <ContainerEvent>{
  172. type: "SERVICE_BIND",
  173. boundeeID: TestServiceA.ID,
  174. bounderID: TestServiceB.ID,
  175. })
  176. expect(serviceFunc).toHaveBeenNthCalledWith(2, <ContainerEvent>{
  177. type: "SERVICE_BIND",
  178. boundeeID: TestServiceB.ID,
  179. bounderID: undefined,
  180. })
  181. })
  182. })
  183. describe("getBoundServices", () => {
  184. it("returns an iterator over all services bound to the container in the format [service id, service instance]", () => {
  185. const container = new Container()
  186. const instanceB = container.bind(TestServiceB)
  187. const instanceA = instanceB.serviceA
  188. expect(Array.from(container.getBoundServices())).toEqual([
  189. [TestServiceA.ID, instanceA],
  190. [TestServiceB.ID, instanceB],
  191. ])
  192. })
  193. it("returns an empty iterator if no services are bound", () => {
  194. const container = new Container()
  195. expect(Array.from(container.getBoundServices())).toEqual([])
  196. })
  197. })
  198. })