182

I want to write a Spek test in Kotlin.
How to read an HTML file from the src/test/resources folder?

class MySpec : Spek(
    {
        describe("blah blah") {
            given("blah blah") {
                var fileContent: String = ""
                beforeEachTest {
                    // How to read the file.html in src/test/resources/html/
                    fileContent = ...
                }
                it("should blah blah") {
                    ...
                }
            }
        }
    }
)
Mahozad
  • 18,032
  • 13
  • 118
  • 133
Olaf
  • 3,786
  • 4
  • 25
  • 38

15 Answers15

204
val fileContent = MySpec::class.java.getResource("/html/file.html").readText()
JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
  • 55
    For me this didn't work, I had to change it to `this::class.java.classLoader.getResource("/html/file.html").readText()` – pavlos163 Sep 15 '17 at 14:19
  • 6
    For me both these options worked in an Android app (notice the extra `/` in one of them, which has to be removed in the other): `this::class.java.getResource("/html/file.html").readText()` and `this::class.java.classLoader.getResource("html/file.html").‌​readText()` – Franco Dec 06 '17 at 02:35
  • 41
    `val fileContent = javaClass.getResource("/html/file.html").readText()` does the job even shorter – Frank Neblung Aug 21 '18 at 10:24
  • 2
    Oddly enough I always need a leading slash. E.g if the file is in the resources root directory, you still have to refer to it as "/file.html" – Somaiah Kumbera Oct 06 '20 at 09:27
  • I tried every solution, does anyone tested this? only is a copy paste of others solutions – fsalazar_sch Sep 27 '21 at 19:33
  • how to read for IOS in KMM – Arun kumar Oct 05 '21 at 10:42
  • 2
    I couldn't get this to work until I read here the difference between `getResource` and `classLoader.getResource` : https://stackoverflow.com/a/6608848/3519951 @SomaiahKumbera leading slash makes the path absolute, see linked post. – JM Lord Feb 14 '22 at 20:53
78

No idea why this is so hard, but the simplest way I've found (without having to refer to a particular class) is:

fun getResourceAsText(path: String): String? =
    object {}.javaClass.getResource(path)?.readText()

It returns null if no resource with this name is found (as documented).

And then passing in an absolute URL, e.g.

val html = getResourceAsText("/www/index.html")!!
cubuspl42
  • 7,833
  • 4
  • 41
  • 65
Russell Briggs
  • 993
  • 8
  • 8
  • 2
    is `{}` required? Why not just `javaClass.getResource(path).readText()`? – andrewgazelka Apr 30 '19 at 20:46
  • 2
    javaClass must be called on an object according to the docs https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/java-class.html If it works without then go for ya life :) – Russell Briggs May 02 '19 at 20:16
  • 4
    One downside with the method above is it creates a new object for every resource access. Would be better to store the dummy object outside the function. – Russell Briggs May 02 '19 at 20:19
  • 2
    @RussellBriggs tbh I don't think that matters much. The performance of an object creation is not really an issue if you do disk access! – Qw3ry Jun 26 '19 at 07:50
  • One snag here is that `javaClass` is apparently nullable? – Hakanai Dec 29 '21 at 02:28
  • @Hakanai I don't think that's the case, but `Class.getResource` does indeed return null if no such resource could be found. I've edited the answer to expose that. – cubuspl42 Jan 25 '22 at 10:03
  • @cubuspl42 I had Kotlin refuse to compile my code unless I `!!` asserted that the value returned from `javaClass` itself wasn't null. So it very much does seem to be the case, and perhaps has something to do with Kotlin supporting running on more than the JVM? – Hakanai Feb 01 '22 at 23:01
  • @Hakanai I've checked the [docs for `javaClass`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/java-class.html) and the result doesn't seem to be nullable. Also, see [this snippet on Kotlin Playground](https://pl.kotl.in/Zzks01Tkm). It compiles correctly. So I'm out of ideas what could make `javaClass` nullable in your case. – cubuspl42 Feb 03 '22 at 18:04
  • Relating to "supporting running on more than the JVM", I'm not really sure what that means. – cubuspl42 Feb 03 '22 at 18:06
  • @cubuspl42 Compose can also run on JavaScript if you're also targeting the web. But yes, when I checked the docs, I saw the same thing. Like I said, it was the _compiler_ which rejected me. I guess it isn't such a hard thing to work around anyway. I already have to deal with the resource itself being null too, so I can just use `?.` to dodge it. – Hakanai Feb 04 '22 at 02:00
32

another slightly different solution:

@Test
fun basicTest() {
    "/html/file.html".asResource {
        // test on `it` here...
        println(it)
    }

}

fun String.asResource(work: (String) -> Unit) {
    val content = this.javaClass::class.java.getResource(this).readText()
    work(content)
}
jhodges
  • 3,065
  • 2
  • 17
  • 9
  • Nice usage of an extension function! But why do you use a lambda function here? That does not make much sense to me. Furthermore, the `this` part did not work for me. Thus I recommend the following: `fun String.asResource(): URL? = object {}.javaClass.getResource(this)` – Qw3ry Jun 26 '19 at 07:48
  • I like this method of working on it where you declare the file, then work on the contents of that file. Personal preference I guess. `this` in the example above refers to the string object. – jhodges Jul 12 '19 at 20:21
  • 26
    that's terrible abuse of extension function. Loading files is not a concern of String class. – Ben Feb 17 '20 at 14:32
  • 2
    it is in this context. i wouldn't make this globally available or use it outside of test classes. I consider it more of a mapping function here. – jhodges Feb 18 '20 at 17:43
  • 2
    @Ben Use of extension functions and extension properties is by definition an abuse, since it pretends to add members to a class it doesn't have internal access to. If you don't like it, don't use it. Some of us are focused on making our code more straightforward rather than worrying about OO purity. – Ryan Lundy Feb 22 '21 at 10:44
  • @RyanLundy no it's not, extension functions are perfectly ok in OOP. The whole idea of extension function is that it does not break encapsulation and remains a pure function, **T.x()** is just a normalized version of **x(T)**. The problem with your code snippet is not that it uses extension function, but that it breaks domain boundaries – Ben Feb 22 '21 at 16:11
  • @Ben how though? is this an impure function? i could write *asResource(filename)* just as well. – jhodges Apr 13 '21 at 15:47
  • @jhodges that's not the point. I'm not implying that your function is impure, I'm implying that it breaks separation of concern. It's not cohesive for the *String* to handle classloader. `fun asResource(path:String, work: (String) -> Unit)` is the proper signature. //// Extension functions are ok in OOP, in fact, they are way better than the hack that "default" keyword is, however they often confuse developers as you can seemingly implement methods where they don't belong. Great feature requires great care – Ben Apr 14 '21 at 09:34
  • When I read code like `"/html/file.html".asResource` I wonder whether the resource contains the literal content "/html/file.html". That's the real problem with putting this on string. It might have been less jarring if the extension function were on `Path`. But I agree that it would be even better to just have a global function like `loadResource(String)` – Hakanai Dec 29 '21 at 02:22
19

A slightly different solution:

class MySpec : Spek({
    describe("blah blah") {
        given("blah blah") {

            var fileContent = ""

            beforeEachTest {
                html = this.javaClass.getResource("/html/file.html").readText()
            }

            it("should blah blah") {
                ...
            }
        }
    }
})
Olaf
  • 3,786
  • 4
  • 25
  • 38
  • For some reason this didn't work for me. Only explicitly calling the class worked. Just adding for others. I think it has something to do with tornadofx – nmu Aug 29 '17 at 19:35
  • 2
    After creating a test input file in `/src/test/resources`, `this.javaClass.getResource("/")` worked as expected. Thanks for the solution above. – jkwuc89 Dec 11 '17 at 14:34
  • what to do, if fileContent is not String and I won't create any dummy object? – minizibi Apr 08 '18 at 13:05
  • 1
    leading slash before the path seems mandatory here, whereas in Java I usually omit it. – cakraww Aug 07 '18 at 05:54
14

Kotlin + Spring way:

@Autowired
private lateinit var resourceLoader: ResourceLoader

fun load() {
    val html = resourceLoader.getResource("classpath:html/file.html").file
        .readText(charset = Charsets.UTF_8)
}
naXa stands with Ukraine
  • 35,493
  • 19
  • 190
  • 259
  • 1
    For me this caused problems later. Whilst i dockerised my jar there was problem with the files missing .. – kristjan reinhold Apr 26 '21 at 09:41
  • @kris what part of this code is causing problems?and how did you solve it? – naXa stands with Ukraine Apr 26 '21 at 10:30
  • with root cause java.io.FileNotFoundException: class path resource [email/next_interaction_deadline.txt] cannot be resolved to absolute file path because it does not reside in the file system. Fix was just to use the first response. – kristjan reinhold Apr 26 '21 at 13:04
  • 1
    "resourceLoader.getResource().file" is what causes FileNotFoundException. There's no File, There is an inputStream instead. So this is the correct way is: "resourceLoader.getResource().inputStream.bufferedReader().use { it.readText() }" – Kessir Apr 06 '22 at 07:10
11

Using Google Guava library Resources class:

import com.google.common.io.Resources;

val fileContent: String = Resources.getResource("/html/file.html").readText()
Ilya Serbis
  • 21,149
  • 6
  • 87
  • 74
  • 2
    it is nice that Guave reports a file name if resource is not found - much better for troubleshooting – Nishi Aug 28 '19 at 17:18
10
val fileContent = javaClass.getResource("/html/file.html").readText()
jivimberg
  • 840
  • 1
  • 10
  • 21
7
private fun loadResource(file: String) = {}::class.java.getResource(file).readText()
Olaf
  • 3,786
  • 4
  • 25
  • 38
7

This is the way that I prefer to do it:

fun getResourceText(path: String): String {
    return File(ClassLoader.getSystemResource(path).file).readText()
}
saidaspen
  • 560
  • 1
  • 5
  • 13
  • This may work for some people, but generally the resource _may not_ be loaded by the system class loader. – Hakanai Dec 29 '21 at 02:40
3

this top-level kotlin function will do the job in any case

fun loadResource(path: String): URL {
    return Thread.currentThread().contextClassLoader.getResource(path)
}

or if you want a more robust function

fun loadResource(path: String): URL {
    val resource = Thread.currentThread().contextClassLoader.getResource(path)
    requireNotNull(resource) { "Resource $path not found" }
    return resource
}
mindlid
  • 1,679
  • 14
  • 17
1

FYI: In all the above cases. getResource() is unsafe way of using nullable.

Haven't tried locally but I prefer this way:

fun readFile(resourcePath: String) = String::class.java.getResource(resourcePath)?.readText() ?: "<handle default. or handle custom exception>"

Or even as custom datatype function

private fun String.asResource() = this::class.java.getResource(resourcePath)?.readText() ?: "<handle default. or handle custom exception>"

and then you can call directly on path like:

// For suppose
val path = "/src/test/resources"
val content = path.asResource()
bh4r4th
  • 3,760
  • 1
  • 21
  • 25
  • Interesting. `this::class.java.getResource` doesn't give me the warning about it being null, whereas `javaClass.getResource` does. In any case putting this as an extension method on string doesn't make a lot of sense. – Hakanai Dec 29 '21 at 02:44
0

I prefer reading resources in this way:

object {}.javaClass.getResourceAsStream("/html/file.html")?.use { it.reader(Charsets.UTF_8).readText() }

Explenation:

  • getResourceAsStream instead getResource. The resource on classpath can be basically anywhere. e.g. packed inside another .jar file.
    In these situations accessing resource via URL class returned from getResource method will fail. But accessing via method getResourceAsStream works in every situation.
  • object {} - This is not nice syntax, but it is not dependent on name of your class MyClass and works even in static (compenion object) block.
  • use to close stream - in most cases it is not necessary, but there can be some special classloaders, which may need it.
  • reader(Charsets.UTF_8) - UTF_8 is default encoding, but I prefer to be explicit. If you will encode your resource files in other encoding e.g. ISO-8859-2 you will not overlook it.
bugs_
  • 3,544
  • 4
  • 34
  • 39
0

Another variation that handles null resource in place:

val content = object {}.javaClass
    .getResource("/html/file.html")
    ?.let(URL::readText)
    ?: error("Cannot open/find the file")
//  ?: "default text" // Instead of error()
Mahozad
  • 18,032
  • 13
  • 118
  • 133
0
fun Any.resourceString(path: String): String =
    this.javaClass.getResource(path)?.readText() ?: error("Can't load resource at $path")

Call to this extension function looks relative to the class where call is made.

æ-ra-code
  • 2,140
  • 30
  • 28
-3

You might find the File class useful:

import java.io.File

fun main(args: Array<String>) {
  val content = File("src/main/resources/input.txt").readText()
  print(content)
} 
  • 11
    This answer is misleading. That does not load a "resource" but loads file straight from file system, instead of the classpath. It will no longer work after application is assembled as you will try to refer to nonexisting files, instead of loading them from the jar file. – Ben Feb 09 '20 at 20:51
  • @ben Thanks for your comment. Question was about reading file from resource in kotlin Spek test. – Krzysztof Ziomek Feb 12 '20 at 13:53